#
tokens: 26047/50000 18/18 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── .xray
│   ├── xray.db
│   ├── xray.db-shm
│   └── xray.db-wal
├── getting_started.md
├── install.sh
├── LICENSE
├── mcp-config-generator.py
├── pyproject.toml
├── README.md
├── src
│   └── xray
│       ├── __init__.py
│       ├── core
│       │   ├── __init__.py
│       │   └── indexer.py
│       ├── lsp_config.json
│       └── mcp_server.py
├── test_samples
│   ├── test_class_expression.js
│   ├── test.go
│   ├── test.js
│   ├── test.py
│   └── test.ts
├── tests
│   └── __init__.py
└── uninstall.sh
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Build artifacts
 2 | build/
 3 | dist/
 4 | *.egg-info/
 5 | 
 6 | # Test artifacts
 7 | .pytest_cache/
 8 | 
 9 | # Node.js dependencies
10 | node_modules/
11 | 
12 | # Python cache
13 | __pycache__/
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # XRAY MCP - Progressive Code Intelligence for AI Assistants
  2 | 
  3 | [![Python](https://img.shields.io/badge/Python-3.10+-green)](https://python.org) [![MCP](https://img.shields.io/badge/MCP-Compatible-purple)](https://modelcontextprotocol.io) [![ast-grep](https://img.shields.io/badge/Powered_by-ast--grep-orange)](https://ast-grep.github.io)
  4 | 
  5 | ## ❌ Without XRAY
  6 | 
  7 | AI assistants struggle with codebase understanding. You get:
  8 | 
  9 | - ❌ "I can't see your code structure"
 10 | - ❌ "I don't know what depends on this function"
 11 | - ❌ Generic refactoring advice without impact analysis
 12 | - ❌ No understanding of symbol relationships
 13 | 
 14 | ## ✅ With XRAY
 15 | 
 16 | XRAY gives AI assistants code navigation capabilities. Add `use XRAY tools` to your prompt:
 17 | 
 18 | ```txt
 19 | Analyze the UserService class and show me what would break if I change the authenticate method. use XRAY tools
 20 | ```
 21 | 
 22 | ```txt
 23 | Find all functions that call validate_user and show their dependencies. use XRAY tools
 24 | ```
 25 | 
 26 | XRAY provides three focused tools:
 27 | 
 28 | - 🗺️ **Map** (`explore_repo`) - See project structure with symbol skeletons
 29 | - 🔍 **Find** (`find_symbol`) - Locate functions and classes with fuzzy search
 30 | - 💥 **Impact** (`what_breaks`) - Find where a symbol is referenced
 31 | 
 32 | ## 🚀 Quick Install
 33 | 
 34 | ### Modern Install with uv (Recommended)
 35 | 
 36 | ```bash
 37 | # Install uv if you don't have it
 38 | curl -LsSf https://astral.sh/uv/install.sh | sh
 39 | 
 40 | # Clone and install XRAY
 41 | git clone https://github.com/srijanshukla18/xray.git
 42 | cd xray
 43 | uv tool install .
 44 | ```
 45 | 
 46 | ### Automated Install with uv
 47 | 
 48 | For the quickest setup, this script automates the `uv` installation process.
 49 | 
 50 | ```bash
 51 | curl -fsSL https://raw.githubusercontent.com/srijanshukla18/xray/main/install.sh | bash
 52 | ```
 53 | 
 54 | ### Generate Config
 55 | 
 56 | ```bash
 57 | # Get config for your tool
 58 | python mcp-config-generator.py cursor local_python
 59 | python mcp-config-generator.py claude docker  
 60 | python mcp-config-generator.py vscode source
 61 | ```
 62 | 
 63 | ## Language Support
 64 | 
 65 | XRAY uses [ast-grep](https://ast-grep.github.io), a tree-sitter powered structural search tool, providing accurate parsing for:
 66 | - **Python** - Functions, classes, methods, async functions
 67 | - **JavaScript** - Functions, classes, arrow functions, imports
 68 | - **TypeScript** - All JavaScript features plus interfaces, type aliases
 69 | - **Go** - Functions, structs, interfaces, methods
 70 | 
 71 | ast-grep ensures structural accuracy - it understands code syntax, not just text patterns.
 72 | 
 73 | ## The XRAY Workflow - Progressive Discovery
 74 | 
 75 | ### 1. Map - Start Simple, Then Zoom In
 76 | ```python
 77 | # First: Get the big picture (directories only)
 78 | tree = explore_repo("/path/to/project")
 79 | # Returns:
 80 | # /path/to/project/
 81 | # ├── src/
 82 | # ├── tests/
 83 | # ├── docs/
 84 | # └── config/
 85 | 
 86 | # Then: Zoom into areas of interest with full details
 87 | tree = explore_repo("/path/to/project", focus_dirs=["src"], include_symbols=True)
 88 | # Returns:
 89 | # /path/to/project/
 90 | # └── src/
 91 | #     ├── auth.py
 92 | #     │   ├── class AuthService: # Handles user authentication
 93 | #     │   ├── def authenticate(username, password): # Validates user credentials
 94 | #     │   └── def logout(session_id): # Ends user session
 95 | #     └── models.py
 96 | #         ├── class User(BaseModel): # User account model
 97 | #         └── ... and 3 more
 98 | 
 99 | # Or: Limit depth for large codebases
100 | tree = explore_repo("/path/to/project", max_depth=2, include_symbols=True)
101 | ```
102 | 
103 | ### 2. Find - Locate Specific Symbols
104 | ```python
105 | # Find symbols matching "authenticate" (fuzzy search)
106 | symbols = find_symbol("/path/to/project", "authenticate")
107 | # Returns list of exact symbol objects with name, type, path, line numbers
108 | ```
109 | 
110 | ### 3. Impact - See What Would Break
111 | ```python
112 | # Find where authenticate_user is used
113 | symbol = symbols[0]  # From find_symbol
114 | result = what_breaks(symbol)
115 | # Returns: {"references": [...], "total_count": 12, 
116 | #          "note": "Found 12 potential references based on text search..."}
117 | ```
118 | 
119 | 
120 | ## Architecture
121 | 
122 | ```
123 | FastMCP Server (mcp_server.py)
124 |     ↓
125 | Core Engine (src/xray/core/)
126 |     └── indexer.py      # Orchestrates ast-grep for structural analysis
127 |     ↓
128 | ast-grep (external binary)
129 |     └── Tree-sitter powered structural search
130 | ```
131 | 
132 | **Stateless design** - No database, no persistent index. Each operation runs fresh ast-grep queries for real-time accuracy.
133 | 
134 | ## Why ast-grep?
135 | 
136 | Traditional grep searches text. ast-grep searches code structure:
137 | 
138 | - **grep**: Finds "authenticate" in function names, variables, comments, strings
139 | - **ast-grep**: Finds only `def authenticate()` or `function authenticate()` definitions
140 | 
141 | This structural approach provides clean, accurate results essential for reliable code intelligence.
142 | 
143 | ## Performance Characteristics
144 | 
145 | - **Startup**: Fast - launches ast-grep subprocess
146 | - **File tree**: Python directory traversal
147 | - **Symbol search**: Runs multiple ast-grep patterns, speed depends on codebase size
148 | - **Impact analysis**: Name-based search across all files
149 | - **Memory**: Minimal - no persistent state
150 | 
151 | ## What Makes This Practical
152 | 
153 | 1. **Progressive Discovery** - Start with directories, add symbols only where needed
154 | 2. **Smart Caching** - Symbol extraction cached per git commit for instant re-runs
155 | 3. **Flexible Focus** - Use `focus_dirs` to zoom into specific parts of large codebases
156 | 4. **Enhanced Symbols** - See function signatures and docstrings, not just names
157 | 5. **Based on tree-sitter** - ast-grep provides accurate structural analysis
158 | 
159 | XRAY helps AI assistants avoid information overload while providing deep code intelligence where needed.
160 | 
161 | ## Stateless Design
162 | 
163 | XRAY performs on-demand structural analysis using ast-grep. There's no database to manage, no index to build, and no state to maintain. Each query runs fresh against your current code.
164 | 
165 | ## Getting Started
166 | 
167 | 1. **Install**: See [`getting_started.md`](getting_started.md) for modern installation
168 | 2. **Map the terrain**: `explore_repo("/path/to/project")`
169 | 3. **Find your target**: `find_symbol("/path/to/project", "UserService")`
170 | 4. **Assess impact**: `what_breaks(symbol)`
171 | 
172 | ## The XRAY Philosophy
173 | 
174 | XRAY bridges the gap between simple text search and complex LSP servers:
175 | 
176 | - **More than grep** - Matches code syntax patterns, not just text
177 | - **Less than LSP** - No language servers or complex setup
178 | - **Practical for AI** - Provides structured data about code relationships
179 | 
180 | A simple tool that helps AI assistants navigate codebases more effectively than text search alone.
181 | 
182 | ## Architectural Journey & Design Rationale
183 | 
184 | The current implementation of XRAY is the result of a rigorous evaluation of multiple code analysis methodologies. My journey involved prototyping and assessing several distinct approaches, each with its own set of trade-offs. Below is a summary of the considered architectures and the rationale for my final decision.
185 | 
186 | 1.  **Naive Grep-Based Analysis**: I initially explored a baseline approach using standard `grep` for symbol identification. While expedient, this method proved fundamentally inadequate due to its inability to differentiate between syntactical constructs and simple text occurrences (e.g., comments, strings, variable names). The high signal-to-noise ratio rendered it impractical for reliable code intelligence.
187 | 
188 | 2.  **Tree-Sitter Native Integration**: A direct integration with `tree-sitter` was evaluated to leverage its powerful parsing capabilities. However, this path was fraught with significant implementation complexities, including intractable errors within the parser generation and binding layers. The maintenance overhead and steep learning curve for custom grammar development were deemed prohibitive for a lean, multi-language tool.
189 | 
190 | 3.  **Language Server Protocol (LSP)**: I considered leveraging the Language Server Protocol for its comprehensive, standardized approach to code analysis. This was ultimately rejected due to the excessive operational burden it would impose on the end-user, requiring them to install, configure, and manage separate LSPs for each language in their environment. This friction conflicted with my goal of a lightweight, zero-configuration user experience.
191 | 
192 | 4.  **Comby-Based Structural Search**: `Comby` was explored for its structural search and replacement capabilities. Despite its promising feature set, I encountered significant runtime instability and idiosyncratic behavior that undermined its reliability for mission-critical code analysis. The tool's performance and consistency did not meet my stringent requirements for a production-ready system.
193 | 
194 | 5.  **ast-grep as the Core Engine**: My final and current architecture is centered on `ast-grep`. This tool provides the optimal balance of structural awareness, performance, and ease of integration. By leveraging `tree-sitter` internally, it offers robust, syntactically-aware code analysis without the complexities of direct `tree-sitter` integration or the overhead of LSPs. Its reliability and rich feature set for structural querying made it the unequivocal choice for XRAY's core engine.
195 | 
196 | ---
197 | 
198 | # Getting Started with XRAY - Modern Installation with uv
199 | 
200 | XRAY is a minimal-dependency code intelligence system that enhances AI assistants' understanding of codebases. This guide shows how to install and use XRAY with the modern `uv` package manager.
201 | 
202 | ## Prerequisites
203 | 
204 | - Python 3.10 or later
205 | - [uv](https://docs.astral.sh/uv/) - Fast Python package manager
206 | 
207 | ### Installing uv
208 | 
209 | ```bash
210 | # macOS/Linux
211 | curl -LsSf https://astral.sh/uv/install.sh | sh
212 | 
213 | # Windows
214 | powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
215 | 
216 | # Or with pip
217 | pip install uv
218 | ```
219 | 
220 | ## Installation Options
221 | 
222 | ### Option 1: Automated Install (Easiest)
223 | 
224 | For the quickest setup, use the one-line installer from the `README.md`. This will handle everything for you.
225 | 
226 | ```bash
227 | curl -fsSL https://raw.githubusercontent.com/srijanshukla18/xray/main/install.sh | bash
228 | ```
229 | 
230 | ### Option 2: Quick Try with uvx (Recommended for Testing)
231 | 
232 | Run XRAY directly without installation using `uvx`:
233 | 
234 | ```bash
235 | # Clone the repository
236 | git clone https://github.com/srijanshukla18/xray.git
237 | cd xray
238 | 
239 | # Run XRAY directly with uvx
240 | uvx --from . xray-mcp
241 | ```
242 | 
243 | ### Option 3: Install as a Tool (Recommended for Regular Use)
244 | 
245 | Install XRAY as a persistent tool:
246 | 
247 | ```bash
248 | # Clone and install
249 | git clone https://github.com/srijanshukla18/xray.git
250 | cd xray
251 | 
252 | # Install with uv
253 | uv tool install .
254 | 
255 | # Now you can run xray-mcp from anywhere
256 | xray-mcp
257 | ```
258 | 
259 | ### Option 4: Development Installation
260 | 
261 | For contributing or modifying XRAY:
262 | 
263 | ```bash
264 | # Clone the repository
265 | git clone https://github.com/srijanshukla18/xray.git
266 | cd xray
267 | 
268 | # Create and activate virtual environment with uv
269 | uv venv
270 | source .venv/bin/activate  # On Windows: .venv\Scripts\activate
271 | 
272 | # Install in editable mode
273 | uv pip install -e .
274 | 
275 | # Run the server
276 | python -m xray.mcp_server
277 | ```
278 | 
279 | ## Configure Your AI Assistant
280 | 
281 | After installation, configure your AI assistant to use XRAY:
282 | 
283 | ### Using the MCP Config Generator (Recommended)
284 | 
285 | For easier configuration, use the `mcp-config-generator.py` script located in the XRAY repository. This script can generate the correct JSON configuration for various AI assistants and installation methods.
286 | 
287 | To use it:
288 | 
289 | 1.  Navigate to the XRAY repository root:
290 |     ```bash
291 |     cd /path/to/xray
292 |     ```
293 | 2.  Run the script with your desired tool and installation method. For example, to get the configuration for Claude Desktop with an installed `xray-mcp` script:
294 |     ```bash
295 |     python mcp-config-generator.py claude installed_script
296 |     ```
297 |     Or for VS Code with a local Python installation:
298 |     ```bash
299 |     python mcp-config-generator.py vscode local_python
300 |     ```
301 |     The script will print the JSON configuration and instructions on where to add it.
302 | 
303 |     Available tools: `cursor`, `claude`, `vscode`
304 |     Available methods: `local_python`, `docker`, `source`, `installed_script` (method availability varies by tool)
305 | 
306 | ### Manual Configuration (Advanced)
307 | 
308 | If you prefer to configure manually, here are examples for common AI assistants:
309 | 
310 | #### Claude CLI (Claude Code)
311 | 
312 | For Claude CLI users, simply run:
313 | 
314 | ```bash
315 | claude mcp add xray xray-mcp -s local
316 | ```
317 | 
318 | Then verify it's connected:
319 | 
320 | ```bash
321 | claude mcp list | grep xray
322 | ```
323 | 
324 | #### Claude Desktop
325 | 
326 | Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
327 | 
328 | ```json
329 | {
330 |   "mcpServers": {
331 |     "xray": {
332 |       "command": "uvx",
333 |       "args": ["--from", "/path/to/xray", "xray-mcp"]
334 |     }
335 |   }
336 | }
337 | ```
338 | 
339 | Or if installed as a tool:
340 | 
341 | ```json
342 | {
343 |   "mcpServers": {
344 |     "xray": {
345 |       "command": "xray-mcp"
346 |     }
347 |   }
348 | }
349 | ```
350 | 
351 | #### Cursor
352 | 
353 | Settings → Cursor Settings → MCP → Add new global MCP server:
354 | 
355 | ```json
356 | {
357 |   "mcpServers": {
358 |     "xray": {
359 |       "command": "xray-mcp"
360 |     }
361 |   }
362 | }
363 | ```
364 | 
365 | ## Minimal Dependencies
366 | 
367 | One of XRAY's best features is its minimal dependency profile. You don't need to install a suite of language servers. XRAY uses:
368 | 
369 | - **ast-grep**: A single, fast binary for structural code analysis.
370 | - **Python**: For the server and core logic.
371 | 
372 | This means you can start using XRAY immediately after installation with no complex setup!
373 | 
374 | ## Verify Installation
375 | 
376 | ### 1. Check XRAY is accessible
377 | 
378 | ```bash
379 | # If installed as tool
380 | xray-mcp --version
381 | 
382 | # If using uvx
383 | uvx --from /path/to/xray xray-mcp --version
384 | ```
385 | 
386 | ### 2. Test basic functionality
387 | 
388 | Create a test file `test_xray.py`:
389 | 
390 | ```python
391 | def hello_world():
392 |     print("Hello from XRAY test!")
393 | 
394 | def calculate_sum(a, b):
395 |     return a + b
396 | 
397 | class Calculator:
398 |     def multiply(self, x, y):
399 |         return x * y
400 | ```
401 | 
402 | ### 3. In your AI assistant, test these commands:
403 | 
404 | ```
405 | Build the index for the current directory. use XRAY tools
406 | ```
407 | 
408 | Expected: Success message with files indexed
409 | 
410 | ```
411 | Find all functions containing "hello". use XRAY tools
412 | ```
413 | 
414 | Expected: Should find `hello_world` function
415 | 
416 | ```
417 | What would break if I change the multiply method? use XRAY tools
418 | ```
419 | 
420 | Expected: Impact analysis showing any dependencies
421 | 
422 | ## Usage Examples
423 | 
424 | Once configured, use XRAY by adding "use XRAY tools" to your prompts:
425 | 
426 | ```
427 | # Index a codebase
428 | "Index the src/ directory for analysis. use XRAY tools"
429 | 
430 | # Find symbols
431 | "Find all classes that contain 'User' in their name. use XRAY tools"
432 | 
433 | # Impact analysis
434 | "What breaks if I change the authenticate method in UserService? use XRAY tools"
435 | 
436 | # Dependency tracking
437 | "What does the PaymentProcessor class depend on? use XRAY tools"
438 | 
439 | # Location queries
440 | "What function is defined at line 125 in main.py? use XRAY tools"
441 | ```
442 | 
443 | ## Troubleshooting
444 | 
445 | ### uv not found
446 | 
447 | Make sure uv is in your PATH:
448 | 
449 | ```bash
450 | # Add to ~/.bashrc or ~/.zshrc
451 | export PATH="$HOME/.cargo/bin:$PATH"
452 | ```
453 | 
454 | ### Permission denied
455 | 
456 | On macOS/Linux, you might need to make the script executable:
457 | 
458 | ```bash
459 | chmod +x ~/.local/bin/xray-mcp
460 | ```
461 | 
462 | ### Python version issues
463 | 
464 | XRAY requires Python 3.10+. Check your version:
465 | 
466 | ```bash
467 | python --version
468 | 
469 | # If needed, install Python 3.10+ with uv
470 | uv python install 3.10
471 | ```
472 | 
473 | ### MCP connection issues
474 | 
475 | 1. Check XRAY is running: `xray-mcp --test`
476 | 2. Verify your MCP config JSON is valid
477 | 3. Restart your AI assistant after config changes
478 | 
479 | ## Advanced Configuration
480 | 
481 | ### Custom Database Location
482 | 
483 | Set the `XRAY_DB_PATH` environment variable:
484 | 
485 | ```bash
486 | export XRAY_DB_PATH="$HOME/.xray/databases"
487 | ```
488 | 
489 | ### Debug Mode
490 | 
491 | Enable debug logging:
492 | 
493 | ```bash
494 | export XRAY_DEBUG=1
495 | ```
496 | 
497 | ## What's Next?
498 | 
499 | 1. **Index your first repository**: In your AI assistant, ask it to "Build the index for my project. use XRAY tools"
500 | 
501 | 2. **Explore the tools**:
502 |    - `build_index` - Visual file tree of your repository
503 |    - `find_symbol` - Fuzzy search for functions, classes, and methods
504 |    - `what_breaks` - Find what code depends on a symbol (reverse dependencies)
505 |    - `what_depends` - Find what a symbol depends on (calls and imports)
506 |    
507 |    Note: Results may include matches from comments or strings. The AI assistant will intelligently filter based on context.
508 | 
509 | 3. **Read the documentation**: Check out the [README](README.md) for detailed examples and API reference
510 | 
511 | ## Why XRAY Uses a Minimal Dependency Approach
512 | 
513 | XRAY is designed for simplicity and ease of use. It relies on:
514 | 
515 | - **ast-grep**: A powerful and fast single-binary tool for code analysis.
516 | - **Python**: For its robust standard library and ease of scripting.
517 | 
518 | This approach avoids the complexity of setting up and managing multiple language servers, while still providing accurate, structural code intelligence.
519 | 
520 | ## Benefits of Using uv
521 | 
522 | - **10-100x faster** than pip for installations
523 | - **No virtual environment hassles** - uv manages everything
524 | - **Reproducible installs** - uv.lock ensures consistency
525 | - **Built-in Python management** - install any Python version
526 | - **Global tool management** - like pipx but faster
527 | 
528 | Happy coding with XRAY! 🚀
529 | 
```

--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """Tests for XRAY."""
```

--------------------------------------------------------------------------------
/src/xray/core/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """Core XRAY functionality."""
```

--------------------------------------------------------------------------------
/test_samples/test_class_expression.js:
--------------------------------------------------------------------------------

```javascript
1 | 
2 | 
3 | 
4 | const myClass = class MyClass {};
```

--------------------------------------------------------------------------------
/src/xray/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """XRAY: Fast code intelligence for AI assistants."""
2 | 
3 | __version__ = "0.1.0"
```

--------------------------------------------------------------------------------
/test_samples/test.js:
--------------------------------------------------------------------------------

```javascript
 1 | // JavaScript test file
 2 | import { readFile } from 'fs';
 3 | import axios from 'axios';
 4 | 
 5 | // Function declaration
 6 | function processData(data) {
 7 |     console.log('Processing data:', data);
 8 |     return data.map(item => item * 2);
 9 | }
10 | 
11 | // Arrow function
12 | const fetchData = async (url) => {
13 |     const response = await axios.get(url);
14 |     return response.data;
15 | };
16 | 
17 | // Class declaration
18 | class DataService {
19 |     constructor(apiUrl) {
20 |         this.apiUrl = apiUrl;
21 |     }
22 |     
23 |     async getData() {
24 |         const data = await fetchData(this.apiUrl);
25 |         return processData(data);
26 |     }
27 |     
28 |     validateData(data) {
29 |         return data && data.length > 0;
30 |     }
31 | }
32 | 
33 | // Function expression
34 | const helper = function(x) {
35 |     return x * 2;
36 | };
37 | 
38 | // Export
39 | export default DataService;
40 | export { processData, fetchData };
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "xray"
 3 | version = "0.6.1"
 4 | description = "XRAY: Progressive code intelligence for AI assistants - Map, Find, Impact"
 5 | readme = "README.md"
 6 | requires-python = ">=3.10"
 7 | license = { text = "MIT" }
 8 | authors = [
 9 |     { name = "Srijan Shukla", email = "[email protected]" }
10 | ]
11 | keywords = ["mcp", "code-intelligence", "ai-assistant", "code-analysis", "ast-grep", "structural-search"]
12 | classifiers = [
13 |     "Development Status :: 4 - Beta",
14 |     "Intended Audience :: Developers",
15 |     "License :: OSI Approved :: MIT License",
16 |     "Programming Language :: Python :: 3",
17 |     "Programming Language :: Python :: 3.10",
18 |     "Programming Language :: Python :: 3.11",
19 |     "Programming Language :: Python :: 3.12",
20 | ]
21 | dependencies = [
22 |     "fastmcp>=0.1.0",
23 |     "ast-grep-cli>=0.39.0",
24 |     "thefuzz>=0.20.0",
25 | ]
26 | 
27 | [project.urls]
28 | Homepage = "https://github.com/srijanshukla18/xray"
29 | Repository = "https://github.com/srijanshukla18/xray"
30 | Issues = "https://github.com/srijanshukla18/xray/issues"
31 | 
32 | [build-system]
33 | requires = ["setuptools>=61.0"]
34 | build-backend = "setuptools.build_meta"
35 | 
36 | [tool.setuptools.packages.find]
37 | where = ["src"]
38 | 
39 | [project.scripts]
40 | xray-mcp = "xray.mcp_server:main"
```

--------------------------------------------------------------------------------
/uninstall.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # XRAY MCP Server Uninstallation Script
 4 | # Usage: bash uninstall.sh
 5 | 
 6 | set -e
 7 | 
 8 | echo "🗑️ Uninstalling XRAY MCP Server..."
 9 | 
10 | # Colors for output
11 | RED='\033[0;31m'
12 | GREEN='\033[0;32m'
13 | YELLOW='\033[1;33m'
14 | NC='\033[0m' # No Color
15 | 
16 | # Check if uv is installed
17 | if ! command -v uv &> /dev/null; then
18 |     echo -e "${RED}❌${NC} uv is not installed. Uninstallation requires uv."
19 |     echo "Please install uv (https://github.com/astral-sh/uv) and try again."
20 |     exit 1
21 | fi
22 | 
23 | # Uninstall the xray tool
24 | echo -e "${YELLOW}🔧${NC} Uninstalling xray tool..."
25 | if uv tool uninstall xray; then
26 |     echo -e "${GREEN}✓${NC} xray tool uninstalled successfully."
27 | else
28 |     echo -e "${YELLOW}⚠${NC} Could not uninstall xray tool. It might not be installed."
29 | fi
30 | 
31 | # Remove the installation directory
32 | INSTALL_DIR="$HOME/.xray"
33 | if [ -d "$INSTALL_DIR" ]; then
34 |     echo -e "${YELLOW}🗑️${NC} Removing installation directory: $INSTALL_DIR"
35 |     rm -rf "$INSTALL_DIR"
36 |     echo -e "${GREEN}✓${NC} Installation directory removed."
37 | fi
38 | 
39 | # Verify uninstallation
40 | if ! command -v xray-mcp &> /dev/null; then
41 |     echo -e "${GREEN}✅ Uninstallation complete!${NC}"
42 | else
43 |     echo -e "${RED}❌ Uninstallation failed.${NC} xray-mcp is still on the PATH."
44 |     echo "This might be due to your shell caching the command. Please restart your shell."
45 | fi 
```

--------------------------------------------------------------------------------
/test_samples/test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // TypeScript test file
 2 | import { EventEmitter } from 'events';
 3 | import type { Request, Response } from 'express';
 4 | 
 5 | // Interface
 6 | interface User {
 7 |     id: number;
 8 |     name: string;
 9 |     email: string;
10 | }
11 | 
12 | // Type alias
13 | type UserRole = 'admin' | 'user' | 'guest';
14 | 
15 | // Enum
16 | enum Status {
17 |     Active = 'ACTIVE',
18 |     Inactive = 'INACTIVE',
19 |     Pending = 'PENDING'
20 | }
21 | 
22 | // Abstract class
23 | abstract class BaseService {
24 |     protected apiUrl: string;
25 |     
26 |     constructor(apiUrl: string) {
27 |         this.apiUrl = apiUrl;
28 |     }
29 |     
30 |     abstract fetchData<T>(): Promise<T>;
31 | }
32 | 
33 | // Generic class
34 | class UserService extends BaseService {
35 |     private users: Map<number, User>;
36 |     
37 |     constructor(apiUrl: string) {
38 |         super(apiUrl);
39 |         this.users = new Map();
40 |     }
41 |     
42 |     async fetchData<User>(): Promise<User> {
43 |         // Implementation
44 |         return {} as User;
45 |     }
46 |     
47 |     addUser(user: User): void {
48 |         this.users.set(user.id, user);
49 |     }
50 |     
51 |     getUser(id: number): User | undefined {
52 |         return this.users.get(id);
53 |     }
54 | }
55 | 
56 | // Function with type annotations
57 | function processUsers(users: User[], role: UserRole): User[] {
58 |     return users.filter(user => {
59 |         // Some filtering logic
60 |         return true;
61 |     });
62 | }
63 | 
64 | // Namespace
65 | namespace Utils {
66 |     export function formatDate(date: Date): string {
67 |         return date.toISOString();
68 |     }
69 |     
70 |     export class Logger {
71 |         log(message: string): void {
72 |             console.log(message);
73 |         }
74 |     }
75 | }
76 | 
77 | // Type guard
78 | function isUser(obj: any): obj is User {
79 |     return obj && typeof obj.id === 'number' && typeof obj.name === 'string';
80 | }
81 | 
82 | export { UserService, processUsers, Status, Utils };
83 | export type { User, UserRole };
```

--------------------------------------------------------------------------------
/test_samples/test.py:
--------------------------------------------------------------------------------

```python
 1 | # Python test file
 2 | import os
 3 | import sys
 4 | from typing import List, Dict, Optional
 5 | from dataclasses import dataclass
 6 | import asyncio
 7 | 
 8 | # Class definition
 9 | class DataProcessor:
10 |     def __init__(self, config: Dict[str, any]):
11 |         self.config = config
12 |         self.data = []
13 |     
14 |     def process(self, items: List[str]) -> List[str]:
15 |         """Process a list of items."""
16 |         return [self._transform(item) for item in items]
17 |     
18 |     def _transform(self, item: str) -> str:
19 |         """Transform a single item."""
20 |         return item.upper()
21 |     
22 |     @staticmethod
23 |     def validate(data: any) -> bool:
24 |         """Validate data."""
25 |         return bool(data)
26 | 
27 | # Dataclass
28 | @dataclass
29 | class User:
30 |     id: int
31 |     name: str
32 |     email: str
33 |     
34 |     def get_display_name(self) -> str:
35 |         return f"{self.name} <{self.email}>"
36 | 
37 | # Function definitions
38 | def fetch_data(url: str) -> Dict[str, any]:
39 |     """Fetch data from a URL."""
40 |     # Implementation
41 |     return {"data": []}
42 | 
43 | async def async_fetch(url: str) -> Dict[str, any]:
44 |     """Async fetch data."""
45 |     await asyncio.sleep(1)
46 |     return fetch_data(url)
47 | 
48 | # Lambda function
49 | transform = lambda x: x * 2
50 | 
51 | # Generator function
52 | def data_generator(n: int):
53 |     """Generate n items."""
54 |     for i in range(n):
55 |         yield i * 2
56 | 
57 | # Using imported modules
58 | def main():
59 |     processor = DataProcessor({"debug": True})
60 |     user = User(1, "John", "[email protected]")
61 |     
62 |     # Function calls
63 |     data = fetch_data("https://api.example.com")
64 |     processed = processor.process(["a", "b", "c"])
65 |     
66 |     # Method calls
67 |     display_name = user.get_display_name()
68 |     is_valid = DataProcessor.validate(data)
69 |     
70 |     print(f"User: {display_name}")
71 |     print(f"Valid: {is_valid}")
72 | 
73 | if __name__ == "__main__":
74 |     main()
```

--------------------------------------------------------------------------------
/test_samples/test.go:
--------------------------------------------------------------------------------

```go
  1 | // Go test file
  2 | package main
  3 | 
  4 | import (
  5 |     "fmt"
  6 |     "net/http"
  7 |     "encoding/json"
  8 |     db "database/sql"
  9 | )
 10 | 
 11 | // Struct definition
 12 | type User struct {
 13 |     ID    int    `json:"id"`
 14 |     Name  string `json:"name"`
 15 |     Email string `json:"email"`
 16 | }
 17 | 
 18 | // Interface definition
 19 | type Service interface {
 20 |     GetUser(id int) (*User, error)
 21 |     CreateUser(user User) error
 22 |     DeleteUser(id int) error
 23 | }
 24 | 
 25 | // Type alias
 26 | type UserID int
 27 | 
 28 | // Constants
 29 | const (
 30 |     MaxUsers = 100
 31 |     DefaultTimeout = 30
 32 | )
 33 | 
 34 | // Variables
 35 | var (
 36 |     userCache map[int]*User
 37 |     logger    *Logger
 38 | )
 39 | 
 40 | // Struct with methods
 41 | type UserService struct {
 42 |     db     *db.DB
 43 |     cache  map[int]*User
 44 | }
 45 | 
 46 | // Method with pointer receiver
 47 | func (s *UserService) GetUser(id int) (*User, error) {
 48 |     if user, ok := s.cache[id]; ok {
 49 |         return user, nil
 50 |     }
 51 |     
 52 |     user := &User{}
 53 |     err := s.db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user.ID, &user.Name, &user.Email)
 54 |     if err != nil {
 55 |         return nil, err
 56 |     }
 57 |     
 58 |     s.cache[id] = user
 59 |     return user, nil
 60 | }
 61 | 
 62 | // Method with value receiver
 63 | func (s UserService) String() string {
 64 |     return fmt.Sprintf("UserService with %d cached users", len(s.cache))
 65 | }
 66 | 
 67 | // Function
 68 | func NewUserService(database *db.DB) *UserService {
 69 |     return &UserService{
 70 |         db:    database,
 71 |         cache: make(map[int]*User),
 72 |     }
 73 | }
 74 | 
 75 | // Generic-like function using interface{}
 76 | func ProcessData(data interface{}) error {
 77 |     switch v := data.(type) {
 78 |     case *User:
 79 |         return processUser(v)
 80 |     case []User:
 81 |         return processUsers(v)
 82 |     default:
 83 |         return fmt.Errorf("unsupported type: %T", v)
 84 |     }
 85 | }
 86 | 
 87 | func processUser(user *User) error {
 88 |     // Process single user
 89 |     logger.Log("Processing user: " + user.Name)
 90 |     return nil
 91 | }
 92 | 
 93 | func processUsers(users []User) error {
 94 |     for _, user := range users {
 95 |         if err := processUser(&user); err != nil {
 96 |             return err
 97 |         }
 98 |     }
 99 |     return nil
100 | }
101 | 
102 | // HTTP handler
103 | func userHandler(w http.ResponseWriter, r *http.Request) {
104 |     service := NewUserService(nil)
105 |     user, err := service.GetUser(1)
106 |     if err != nil {
107 |         http.Error(w, err.Error(), http.StatusInternalServerError)
108 |         return
109 |     }
110 |     
111 |     json.NewEncoder(w).Encode(user)
112 | }
113 | 
114 | // Logger type
115 | type Logger struct {
116 |     prefix string
117 | }
118 | 
119 | func (l *Logger) Log(message string) {
120 |     fmt.Printf("%s: %s\n", l.prefix, message)
121 | }
122 | 
123 | func main() {
124 |     logger = &Logger{prefix: "APP"}
125 |     userCache = make(map[int]*User)
126 |     
127 |     http.HandleFunc("/user", userHandler)
128 |     http.ListenAndServe(":8080", nil)
129 | }
```

--------------------------------------------------------------------------------
/src/xray/lsp_config.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "languages": [
  3 |     {
  4 |       "id": "python",
  5 |       "name": "Python",
  6 |       "file_extensions": [".py", ".pyw"],
  7 |       "server_executable": "pyright-langserver",
  8 |       "server_args": ["--stdio"],
  9 |       "install_check_command": ["npm", "--version"],
 10 |       "install_check_name": "npm",
 11 |       "install_command": ["npm", "install", "-g", "pyright"],
 12 |       "install_prompt": "To analyze Python files, XRAY needs to install the 'Pyright' language server from Microsoft. This is a one-time setup.",
 13 |       "prerequisite_error": "npm is not installed. Please install Node.js (which includes npm) to enable Python analysis support.",
 14 |       "installation_docs": "https://nodejs.org/",
 15 |       "capabilities": {
 16 |         "documentSymbol": true,
 17 |         "references": true,
 18 |         "definition": true,
 19 |         "typeDefinition": true
 20 |       }
 21 |     },
 22 |     {
 23 |       "id": "javascript",
 24 |       "name": "JavaScript",
 25 |       "file_extensions": [".js", ".jsx", ".mjs", ".cjs"],
 26 |       "server_executable": "typescript-language-server",
 27 |       "server_args": ["--stdio"],
 28 |       "install_check_command": ["npm", "--version"],
 29 |       "install_check_name": "npm",
 30 |       "install_command": ["npm", "install", "-g", "typescript", "typescript-language-server"],
 31 |       "install_prompt": "To analyze JavaScript files, XRAY needs to install the TypeScript Language Server. This is a one-time setup.",
 32 |       "prerequisite_error": "npm is not installed. Please install Node.js (which includes npm) to enable JavaScript analysis support.",
 33 |       "installation_docs": "https://nodejs.org/",
 34 |       "capabilities": {
 35 |         "documentSymbol": true,
 36 |         "references": true,
 37 |         "definition": true,
 38 |         "typeDefinition": true
 39 |       }
 40 |     },
 41 |     {
 42 |       "id": "typescript",
 43 |       "name": "TypeScript",
 44 |       "file_extensions": [".ts", ".tsx", ".mts", ".cts"],
 45 |       "server_executable": "typescript-language-server",
 46 |       "server_args": ["--stdio"],
 47 |       "install_check_command": ["npm", "--version"],
 48 |       "install_check_name": "npm",
 49 |       "install_command": ["npm", "install", "-g", "typescript", "typescript-language-server"],
 50 |       "install_prompt": "To analyze TypeScript files, XRAY needs to install the TypeScript Language Server. This is a one-time setup.",
 51 |       "prerequisite_error": "npm is not installed. Please install Node.js (which includes npm) to enable TypeScript analysis support.",
 52 |       "installation_docs": "https://nodejs.org/",
 53 |       "capabilities": {
 54 |         "documentSymbol": true,
 55 |         "references": true,
 56 |         "definition": true,
 57 |         "typeDefinition": true
 58 |       }
 59 |     },
 60 |     {
 61 |       "id": "go",
 62 |       "name": "Go",
 63 |       "file_extensions": [".go"],
 64 |       "server_executable": "gopls",
 65 |       "server_args": [],
 66 |       "install_check_command": ["go", "version"],
 67 |       "install_check_name": "go",
 68 |       "install_command": ["go", "install", "golang.org/x/tools/gopls@latest"],
 69 |       "install_prompt": "To analyze Go files, XRAY needs to install 'gopls', the official Go language server. This is a one-time setup.",
 70 |       "prerequisite_error": "go is not installed. Please install Go to enable Go analysis support.",
 71 |       "installation_docs": "https://golang.org/doc/install",
 72 |       "capabilities": {
 73 |         "documentSymbol": true,
 74 |         "references": true,
 75 |         "definition": true,
 76 |         "typeDefinition": true
 77 |       }
 78 |     }
 79 |   ],
 80 |   "lsp_settings": {
 81 |     "connection_timeout": 10,
 82 |     "request_timeout": 5,
 83 |     "max_retries": 3,
 84 |     "health_check_interval": 30,
 85 |     "workspace_capabilities": {
 86 |       "applyEdit": true,
 87 |       "workspaceEdit": {
 88 |         "documentChanges": true
 89 |       },
 90 |       "didChangeConfiguration": {
 91 |         "dynamicRegistration": true
 92 |       },
 93 |       "didChangeWatchedFiles": {
 94 |         "dynamicRegistration": true
 95 |       }
 96 |     }
 97 |   },
 98 |   "server_settings": {
 99 |     "initialization_timeout": 60,
100 |     "request_timeout": 30,
101 |     "pyright_initialization_timeout": 120
102 |   }
103 | }
```

--------------------------------------------------------------------------------
/mcp-config-generator.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | XRAY MCP Configuration Generator
  4 | Generates MCP config for different tools and installation methods.
  5 | """
  6 | 
  7 | import json
  8 | import sys
  9 | import os
 10 | from pathlib import Path
 11 | 
 12 | CONFIGS = {
 13 |     "cursor": {
 14 |         "local_python": {
 15 |             "mcpServers": {
 16 |                 "xray": {
 17 |                     "command": "python",
 18 |                     "args": ["-m", "xray.mcp_server"]
 19 |                 }
 20 |             }
 21 |         },
 22 |         "docker": {
 23 |             "mcpServers": {
 24 |                 "xray": {
 25 |                     "command": "docker", 
 26 |                     "args": ["run", "--rm", "-i", "xray"]
 27 |                 }
 28 |             }
 29 |         },
 30 |         "source": {
 31 |             "mcpServers": {
 32 |                 "xray": {
 33 |                     "command": "python",
 34 |                     "args": ["run_server.py"],
 35 |                     "cwd": str(Path.cwd())
 36 |                 }
 37 |             }
 38 |         },
 39 |         "installed_script": {
 40 |             "mcpServers": {
 41 |                 "xray": {
 42 |                     "command": "xray-mcp"
 43 |                 }
 44 |             }
 45 |         }
 46 |     },
 47 |     "claude": {
 48 |         "local_python": {
 49 |             "mcpServers": {
 50 |                 "xray": {
 51 |                     "command": "python",
 52 |                     "args": ["-m", "xray.mcp_server"]
 53 |                 }
 54 |             }
 55 |         },
 56 |         "docker": {
 57 |             "mcpServers": {
 58 |                 "xray": {
 59 |                     "command": "docker",
 60 |                     "args": ["run", "--rm", "-i", "xray"]
 61 |                 }
 62 |             }
 63 |         }
 64 |     },
 65 |     "vscode": {
 66 |         "local_python": {
 67 |             "mcp": {
 68 |                 "servers": {
 69 |                     "xray": {
 70 |                         "type": "stdio",
 71 |                         "command": "python",
 72 |                         "args": ["-m", "xray.mcp_server"]
 73 |                     }
 74 |                 }
 75 |             }
 76 |         },
 77 |         "docker": {
 78 |             "mcp": {
 79 |                 "servers": {
 80 |                     "xray": {
 81 |                         "type": "stdio",
 82 |                         "command": "docker", 
 83 |                         "args": ["run", "--rm", "-i", "xray"]
 84 |                     }
 85 |                 }
 86 |             }
 87 |         },
 88 |         "installed_script": {
 89 |             "mcp": {
 90 |                 "servers": {
 91 |                     "xray": {
 92 |                         "type": "stdio",
 93 |                         "command": "xray-mcp"
 94 |                     }
 95 |                 }
 96 |             }
 97 |         }
 98 |     }
 99 | }
100 | 
101 | def print_config(tool, method):
102 |     """Print MCP configuration for specified tool and method."""
103 |     if tool not in CONFIGS:
104 |         print(f"❌ Unknown tool: {tool}")
105 |         print(f"Available tools: {', '.join(CONFIGS.keys())}")
106 |         return False
107 |     
108 |     if method not in CONFIGS[tool]:
109 |         print(f"❌ Unknown method: {method}")
110 |         print(f"Available methods for {tool}: {', '.join(CONFIGS[tool].keys())}")
111 |         return False
112 |     
113 |     config = CONFIGS[tool][method]
114 |     print(f"🔧 {tool.title()} configuration ({method.replace('_', ' ')}):")
115 |     print()
116 |     print(json.dumps(config, indent=2))
117 |     print()
118 |     
119 |     # Add helpful instructions
120 |     if tool == "cursor":
121 |         print("📝 Add this to your Cursor ~/.cursor/mcp.json file")
122 |     elif tool == "claude":
123 |         print("📝 Add this to your Claude desktop config:")
124 |         print("   macOS: ~/Library/Application Support/Claude/claude_desktop_config.json")
125 |         print("   Windows: %APPDATA%\\Claude\\claude_desktop_config.json")
126 |     elif tool == "vscode":
127 |         print("📝 Add this to your VS Code settings.json file")
128 |     
129 |     return True
130 | 
131 | def main():
132 |     if len(sys.argv) != 3:
133 |         print("XRAY MCP Configuration Generator")
134 |         print()
135 |         print("Usage: python mcp-config-generator.py <tool> <method>")
136 |         print()
137 |         print("Available tools:")
138 |         for tool in CONFIGS:
139 |             methods = ", ".join(CONFIGS[tool].keys())
140 |             print(f"  {tool}: {methods}")
141 |         print()
142 |         print("Examples:")
143 |         print("  python mcp-config-generator.py cursor local_python")
144 |         print("  python mcp-config-generator.py claude docker")
145 |         print("  python mcp-config-generator.py vscode source")
146 |         return 1
147 |     
148 |     tool = sys.argv[1].lower()
149 |     method = sys.argv[2].lower()
150 |     
151 |     if print_config(tool, method):
152 |         return 0
153 |     else:
154 |         return 1
155 | 
156 | if __name__ == "__main__":
157 |     sys.exit(main())
```

--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------

```bash
  1 | #!/bin/bash
  2 | 
  3 | # XRAY MCP Server Installation Script (uv version)
  4 | # Usage: curl -fsSL https://raw.githubusercontent.com/srijanshukla18/xray/main/install.sh | bash
  5 | 
  6 | set -e
  7 | 
  8 | # Check if XRAY is already installed and on the PATH
  9 | if command -v xray-mcp &>/dev/null; then
 10 |     echo -e "${GREEN}✓${NC} XRAY is already installed."
 11 |     # Optionally, ask to reinstall
 12 |     read -p "Do you want to reinstall? (y/N) " -n 1 -r
 13 |     echo
 14 |     if [[ ! $REPLY =~ ^[Yy]$ ]]; then
 15 |         exit 0
 16 |     fi
 17 | fi
 18 | 
 19 | echo "🚀 Installing XRAY MCP Server with uv..."
 20 | 
 21 | # Colors for output
 22 | RED='\033[0;31m'
 23 | GREEN='\033[0;32m'
 24 | YELLOW='\033[1;33m'
 25 | NC='\033[0m' # No Color
 26 | 
 27 | # Check if Python 3.10+ is available
 28 | if command -v python3.11 &>/dev/null; then
 29 |     PYTHON_CMD="python3.11"
 30 | elif command -v python3 &>/dev/null; then
 31 |     PYTHON_CMD="python3"
 32 | else
 33 |     echo -e "${RED}❌${NC} Python 3 is not installed."
 34 |     exit 1
 35 | fi
 36 | 
 37 | PYTHON_VERSION=$($PYTHON_CMD -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
 38 | PYTHON_MAJOR=$(echo $PYTHON_VERSION | cut -d. -f1)
 39 | PYTHON_MINOR=$(echo $PYTHON_VERSION | cut -d. -f2)
 40 | 
 41 | if [ "$PYTHON_MAJOR" -eq 3 ] && [ "$PYTHON_MINOR" -ge 10 ]; then
 42 |     echo -e "${GREEN}✓${NC} Found Python $PYTHON_VERSION"
 43 | else
 44 |     echo -e "${RED}❌${NC} Python $PYTHON_VERSION found, but 3.10+ is required"
 45 |     exit 1
 46 | fi
 47 | 
 48 | # Check if uv is installed
 49 | if ! command -v uv &> /dev/null; then
 50 |     echo -e "${YELLOW}📦${NC} Installing uv..."
 51 |     
 52 |     # Detect OS
 53 |     if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
 54 |         echo "Please install uv on Windows using:"
 55 |         echo "  powershell -c \"irm https://astral.sh/uv/install.ps1 | iex\""
 56 |         exit 1
 57 |     else
 58 |         # macOS and Linux
 59 |         curl -LsSf https://astral.sh/uv/install.sh | sh
 60 |         
 61 |         # Add to PATH for current session
 62 |         export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
 63 |         
 64 |         # Verify installation
 65 |         if command -v uv &> /dev/null; then
 66 |             echo -e "${GREEN}✓${NC} uv installed successfully"
 67 |         else
 68 |             echo -e "${RED}❌${NC} Failed to install uv"
 69 |             exit 1
 70 |         fi
 71 |     fi
 72 | else
 73 |     echo -e "${GREEN}✓${NC} uv is already installed"
 74 | fi
 75 | 
 76 | # Determine installation directory
 77 | if git rev-parse --is-inside-work-tree &> /dev/null; then
 78 |     INSTALL_DIR=$(pwd)
 79 |     echo -e "${GREEN}✓${NC} Installing from current Git repository: $INSTALL_DIR"
 80 |     SKIP_CLONE=true
 81 | else
 82 |     INSTALL_DIR="$HOME/.xray"
 83 |     echo -e "${YELLOW}📦${NC} Installing to default directory: $INSTALL_DIR"
 84 |     SKIP_CLONE=false
 85 | fi
 86 | mkdir -p "$INSTALL_DIR"
 87 | 
 88 | # Clone or update XRAY (only if not installing from current repo)
 89 | if [ "$SKIP_CLONE" = false ]; then
 90 |     echo -e "${YELLOW}📥${NC} Downloading XRAY..."
 91 |     if [ -d "$INSTALL_DIR/.git" ]; then
 92 |         cd "$INSTALL_DIR"
 93 |         echo -e "${YELLOW}🔄${NC} Updating existing installation..."
 94 |         if ! git pull origin main; then
 95 |             echo -e "${YELLOW}⚠${NC} Git pull failed. Performing clean installation..."
 96 |             cd "$HOME"
 97 |             rm -rf "$INSTALL_DIR"
 98 |             git clone https://github.com/srijanshukla18/xray.git "$INSTALL_DIR"
 99 |             cd "$INSTALL_DIR"
100 |         fi
101 |     elif [ -d "$INSTALL_DIR" ]; then
102 |         echo -e "${YELLOW}⚠${NC} Directory exists but is not a git repository. Cleaning up..."
103 |         rm -rf "$INSTALL_DIR"
104 |         git clone https://github.com/srijanshukla18/xray.git "$INSTALL_DIR"
105 |         cd "$INSTALL_DIR"
106 |     else
107 |         git clone https://github.com/srijanshukla18/xray.git "$INSTALL_DIR"
108 |         cd "$INSTALL_DIR"
109 |     fi
110 | else
111 |     # If installing from current repo, just change to it for uv tool install
112 |     cd "$INSTALL_DIR"
113 |     # Clean uv cache to ensure local changes are picked up
114 |     echo -e "${YELLOW}🧹${NC} Cleaning uv cache..."
115 |     uv clean
116 | fi
117 | 
118 | # Install XRAY as a uv tool
119 | echo -e "${YELLOW}🔧${NC} Installing XRAY with uv..."
120 | uv tool install . --force
121 | 
122 | # Add uv's bin directory to PATH for future sessions
123 | uv tool update-shell
124 | 
125 | # Ensure the current shell can find the freshly installed binary
126 | export PATH="$HOME/.local/bin:$PATH"
127 | 
128 | # Verify installation
129 | if command -v xray-mcp &> /dev/null; then
130 |     echo -e "${GREEN}✓${NC} XRAY installed successfully!"
131 | else
132 |     echo -e "${RED}❌${NC} Installation failed"
133 |     exit 1
134 | fi
135 | 
136 | # Run verification test
137 | echo -e "${YELLOW}🧪${NC} Running installation test..."
138 | cd "$INSTALL_DIR"
139 | if $PYTHON_CMD test_installation.py; then
140 |     echo -e "${GREEN}✓${NC} All tests passed!"
141 | else
142 |     echo -e "${YELLOW}⚠${NC} Some tests failed, but installation completed"
143 | fi
144 | 
145 | # Show next steps
146 | echo ""
147 | echo -e "${GREEN}✅ XRAY installed successfully!${NC}"
148 | echo ""
149 | echo "🎯 Quick Start:"
150 | echo "1. Add this to your MCP config:"
151 | echo '   {"mcpServers": {"xray": {"command": "xray-mcp"}}}'
152 | echo ""
153 | echo "2. Use in prompts:"
154 | echo '   "Analyze this codebase for dependencies. use XRAY tools"'
155 | echo ""
156 | echo "📚 Full documentation:"
157 | echo "   https://github.com/srijanshukla18/xray"
158 | echo ""
159 | echo "💡 Tip: You can also run XRAY without installation using:"
160 | echo "   uvx --from $INSTALL_DIR xray-mcp"
161 | 
```

--------------------------------------------------------------------------------
/getting_started.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Getting Started with XRAY - Modern Installation with uv
  2 | 
  3 | XRAY is a minimal-dependency code intelligence system that enhances AI assistants' understanding of codebases. This guide shows how to install and use XRAY with the modern `uv` package manager.
  4 | 
  5 | ## Prerequisites
  6 | 
  7 | - Python 3.10 or later
  8 | - [uv](https://docs.astral.sh/uv/) - Fast Python package manager
  9 | 
 10 | ### Installing uv
 11 | 
 12 | ```bash
 13 | # macOS/Linux
 14 | curl -LsSf https://astral.sh/uv/install.sh | sh
 15 | 
 16 | # Windows
 17 | powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
 18 | 
 19 | # Or with pip
 20 | pip install uv
 21 | ```
 22 | 
 23 | ## Installation Options
 24 | 
 25 | ### Option 1: Automated Install (Easiest)
 26 | 
 27 | For the quickest setup, use the one-line installer from the `README.md`. This will handle everything for you.
 28 | 
 29 | ```bash
 30 | curl -fsSL https://raw.githubusercontent.com/srijanshukla18/xray/main/install.sh | bash
 31 | ```
 32 | 
 33 | ### Option 2: Quick Try with uvx (Recommended for Testing)
 34 | 
 35 | Run XRAY directly without installation using `uvx`:
 36 | 
 37 | ```bash
 38 | # Clone the repository
 39 | git clone https://github.com/srijanshukla18/xray.git
 40 | cd xray
 41 | 
 42 | # Run XRAY directly with uvx
 43 | uvx --from . xray-mcp
 44 | ```
 45 | 
 46 | ### Option 3: Install as a Tool (Recommended for Regular Use)
 47 | 
 48 | Install XRAY as a persistent tool:
 49 | 
 50 | ```bash
 51 | # Clone and install
 52 | git clone https://github.com/srijanshukla18/xray.git
 53 | cd xray
 54 | 
 55 | # Install with uv
 56 | uv tool install .
 57 | 
 58 | # Now you can run xray-mcp from anywhere
 59 | xray-mcp
 60 | ```
 61 | 
 62 | ### Option 4: Development Installation
 63 | 
 64 | For contributing or modifying XRAY:
 65 | 
 66 | ```bash
 67 | # Clone the repository
 68 | git clone https://github.com/srijanshukla18/xray.git
 69 | cd xray
 70 | 
 71 | # Create and activate virtual environment with uv
 72 | uv venv
 73 | source .venv/bin/activate  # On Windows: .venv\Scripts\activate
 74 | 
 75 | # Install in editable mode
 76 | uv pip install -e .
 77 | 
 78 | # Run the server
 79 | python -m xray.mcp_server
 80 | ```
 81 | 
 82 | ## Configure Your AI Assistant
 83 | 
 84 | After installation, configure your AI assistant to use XRAY:
 85 | 
 86 | ### Using the MCP Config Generator (Recommended)
 87 | 
 88 | For easier configuration, use the `mcp-config-generator.py` script located in the XRAY repository. This script can generate the correct JSON configuration for various AI assistants and installation methods.
 89 | 
 90 | To use it:
 91 | 
 92 | 1.  Navigate to the XRAY repository root:
 93 |     ```bash
 94 |     cd /path/to/xray
 95 |     ```
 96 | 2.  Run the script with your desired tool and installation method. For example, to get the configuration for Claude Desktop with an installed `xray-mcp` script:
 97 |     ```bash
 98 |     python mcp-config-generator.py claude installed_script
 99 |     ```
100 |     Or for VS Code with a local Python installation:
101 |     ```bash
102 |     python mcp-config-generator.py vscode local_python
103 |     ```
104 |     The script will print the JSON configuration and instructions on where to add it.
105 | 
106 |     Available tools: `cursor`, `claude`, `vscode`
107 |     Available methods: `local_python`, `docker`, `source`, `installed_script` (method availability varies by tool)
108 | 
109 | ### Manual Configuration (Advanced)
110 | 
111 | If you prefer to configure manually, here are examples for common AI assistants:
112 | 
113 | #### Claude CLI (Claude Code)
114 | 
115 | For Claude CLI users, simply run:
116 | 
117 | ```bash
118 | claude mcp add xray xray-mcp -s local
119 | ```
120 | 
121 | Then verify it's connected:
122 | 
123 | ```bash
124 | claude mcp list | grep xray
125 | ```
126 | 
127 | #### Claude Desktop
128 | 
129 | Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
130 | 
131 | ```json
132 | {
133 |   "mcpServers": {
134 |     "xray": {
135 |       "command": "uvx",
136 |       "args": ["--from", "/path/to/xray", "xray-mcp"]
137 |     }
138 |   }
139 | }
140 | ```
141 | 
142 | Or if installed as a tool:
143 | 
144 | ```json
145 | {
146 |   "mcpServers": {
147 |     "xray": {
148 |       "command": "xray-mcp"
149 |     }
150 |   }
151 | }
152 | ```
153 | 
154 | #### Cursor
155 | 
156 | Settings → Cursor Settings → MCP → Add new global MCP server:
157 | 
158 | ```json
159 | {
160 |   "mcpServers": {
161 |     "xray": {
162 |       "command": "xray-mcp"
163 |     }
164 |   }
165 | }
166 | ```
167 | 
168 | ## Minimal Dependencies
169 | 
170 | One of XRAY's best features is its minimal dependency profile. You don't need to install a suite of language servers. XRAY uses:
171 | 
172 | - **ast-grep**: A single, fast binary for structural code analysis.
173 | - **Python**: For the server and core logic.
174 | 
175 | This means you can start using XRAY immediately after installation with no complex setup!
176 | 
177 | ## Verify Installation
178 | 
179 | ### 1. Check XRAY is accessible
180 | 
181 | ```bash
182 | # If installed as tool
183 | xray-mcp --version
184 | 
185 | # If using uvx
186 | uvx --from /path/to/xray xray-mcp --version
187 | ```
188 | 
189 | ### 2. Test basic functionality
190 | 
191 | Create a test file `test_xray.py`:
192 | 
193 | ```python
194 | def hello_world():
195 |     print("Hello from XRAY test!")
196 | 
197 | def calculate_sum(a, b):
198 |     return a + b
199 | 
200 | class Calculator:
201 |     def multiply(self, x, y):
202 |         return x * y
203 | ```
204 | 
205 | ### 3. In your AI assistant, test these commands:
206 | 
207 | ```
208 | Build the index for the current directory. use XRAY tools
209 | ```
210 | 
211 | Expected: Success message with files indexed
212 | 
213 | ```
214 | Find all functions containing "hello". use XRAY tools
215 | ```
216 | 
217 | Expected: Should find `hello_world` function
218 | 
219 | ```
220 | What would break if I change the multiply method? use XRAY tools
221 | ```
222 | 
223 | Expected: Impact analysis showing any dependencies
224 | 
225 | ## Usage Examples
226 | 
227 | Once configured, use XRAY by adding "use XRAY tools" to your prompts:
228 | 
229 | ```
230 | # Index a codebase
231 | "Index the src/ directory for analysis. use XRAY tools"
232 | 
233 | # Find symbols
234 | "Find all classes that contain 'User' in their name. use XRAY tools"
235 | 
236 | # Impact analysis
237 | "What breaks if I change the authenticate method in UserService? use XRAY tools"
238 | 
239 | # Dependency tracking
240 | "What does the PaymentProcessor class depend on? use XRAY tools"
241 | 
242 | # Location queries
243 | "What function is defined at line 125 in main.py? use XRAY tools"
244 | ```
245 | 
246 | ## Troubleshooting
247 | 
248 | ### uv not found
249 | 
250 | Make sure uv is in your PATH:
251 | 
252 | ```bash
253 | # Add to ~/.bashrc or ~/.zshrc
254 | export PATH="$HOME/.cargo/bin:$PATH"
255 | ```
256 | 
257 | ### Permission denied
258 | 
259 | On macOS/Linux, you might need to make the script executable:
260 | 
261 | ```bash
262 | chmod +x ~/.local/bin/xray-mcp
263 | ```
264 | 
265 | ### Python version issues
266 | 
267 | XRAY requires Python 3.10+. Check your version:
268 | 
269 | ```bash
270 | python --version
271 | 
272 | # If needed, install Python 3.10+ with uv
273 | uv python install 3.10
274 | ```
275 | 
276 | ### MCP connection issues
277 | 
278 | 1. Check XRAY is running: `xray-mcp --test`
279 | 2. Verify your MCP config JSON is valid
280 | 3. Restart your AI assistant after config changes
281 | 
282 | ## Advanced Configuration
283 | 
284 | ### Custom Database Location
285 | 
286 | Set the `XRAY_DB_PATH` environment variable:
287 | 
288 | ```bash
289 | export XRAY_DB_PATH="$HOME/.xray/databases"
290 | ```
291 | 
292 | ### Debug Mode
293 | 
294 | Enable debug logging:
295 | 
296 | ```bash
297 | export XRAY_DEBUG=1
298 | ```
299 | 
300 | ## What's Next?
301 | 
302 | 1. **Index your first repository**: In your AI assistant, ask it to "Build the index for my project. use XRAY tools"
303 | 
304 | 2. **Explore the tools**:
305 |    - `build_index` - Visual file tree of your repository
306 |    - `find_symbol` - Fuzzy search for functions, classes, and methods
307 |    - `what_breaks` - Find what code depends on a symbol (reverse dependencies)
308 |    - `what_depends` - Find what a symbol depends on (calls and imports)
309 |    
310 |    Note: Results may include matches from comments or strings. The AI assistant will intelligently filter based on context.
311 | 
312 | 3. **Read the documentation**: Check out the [README](README.md) for detailed examples and API reference
313 | 
314 | ## Why XRAY Uses a Minimal Dependency Approach
315 | 
316 | XRAY is designed for simplicity and ease of use. It relies on:
317 | 
318 | - **ast-grep**: A powerful and fast single-binary tool for code analysis.
319 | - **Python**: For its robust standard library and ease of scripting.
320 | 
321 | This approach avoids the complexity of setting up and managing multiple language servers, while still providing accurate, structural code intelligence.
322 | 
323 | ## Benefits of Using uv
324 | 
325 | - **10-100x faster** than pip for installations
326 | - **No virtual environment hassles** - uv manages everything
327 | - **Reproducible installs** - uv.lock ensures consistency
328 | - **Built-in Python management** - install any Python version
329 | - **Global tool management** - like pipx but faster
330 | 
331 | Happy coding with XRAY! 🚀
```

--------------------------------------------------------------------------------
/src/xray/mcp_server.py:
--------------------------------------------------------------------------------

```python
  1 | import sys
  2 | from pathlib import Path
  3 | sys.path.insert(0, str(Path(__file__).parent.parent.parent))
  4 | 
  5 | """XRAY MCP Server - Progressive code discovery in 3 steps: Map, Find, Impact.
  6 | 
  7 | 🚀 THE XRAY WORKFLOW (Progressive Discovery):
  8 | 1. explore_repo() - Start with directory structure, then zoom in with symbols
  9 | 2. find_symbol() - Find specific functions/classes you need to analyze  
 10 | 3. what_breaks() - See where that symbol is used (impact analysis)
 11 | 
 12 | PROGRESSIVE DISCOVERY EXAMPLE:
 13 | ```python
 14 | # Step 1a: Get the lay of the land (directories only)
 15 | tree = explore_repo("/Users/john/myproject")
 16 | # Shows directory structure - fast and clean
 17 | 
 18 | # Step 1b: Zoom into interesting areas with symbols
 19 | tree = explore_repo("/Users/john/myproject", focus_dirs=["src"], include_symbols=True)
 20 | # Now shows function signatures and docstrings in src/
 21 | 
 22 | # Step 2: Find the specific function you need
 23 | symbols = find_symbol("/Users/john/myproject", "validate user")
 24 | # Returns list of matching symbols with exact locations
 25 | 
 26 | # Step 3: See what would be affected if you change it
 27 | impact = what_breaks(symbols[0])  # Pass the ENTIRE symbol object!
 28 | # Shows every place that symbol name appears
 29 | ```
 30 | 
 31 | KEY FEATURES:
 32 | - Progressive Discovery: Start simple (dirs only), then add detail where needed
 33 | - Smart Caching: Symbol extraction cached per git commit for instant re-runs
 34 | - Focus Control: Use focus_dirs to examine specific parts of large codebases
 35 | 
 36 | TIPS:
 37 | - Always use ABSOLUTE paths (e.g., "/Users/john/project"), not relative paths
 38 | - Start explore_repo with include_symbols=False to avoid information overload
 39 | - find_symbol uses fuzzy matching - "auth" finds "authenticate", "authorization", etc.
 40 | - what_breaks does text search - review results to see which are actual code references
 41 | """
 42 | 
 43 | import os
 44 | from typing import Dict, List, Any, Optional, Union
 45 | 
 46 | from fastmcp import FastMCP
 47 | 
 48 | from xray.core.indexer import XRayIndexer
 49 | 
 50 | # Initialize FastMCP server
 51 | mcp = FastMCP("XRAY Code Intelligence")
 52 | 
 53 | # Cache for indexer instances per repository path
 54 | _indexer_cache: Dict[str, XRayIndexer] = {}
 55 | 
 56 | 
 57 | def normalize_path(path: str) -> str:
 58 |     """Normalize a path to absolute form."""
 59 |     path = os.path.expanduser(path)
 60 |     path = os.path.abspath(path)
 61 |     path = str(Path(path).resolve())
 62 |     if not os.path.exists(path):
 63 |         raise ValueError(f"Path '{path}' does not exist")
 64 |     if not os.path.isdir(path):
 65 |         raise ValueError(f"Path '{path}' is not a directory")
 66 |     return path
 67 | 
 68 | 
 69 | def get_indexer(path: str) -> XRayIndexer:
 70 |     """Get or create indexer instance for the given path."""
 71 |     path = normalize_path(path)
 72 |     if path not in _indexer_cache:
 73 |         _indexer_cache[path] = XRayIndexer(path)
 74 |     return _indexer_cache[path]
 75 | 
 76 | 
 77 | @mcp.tool
 78 | def explore_repo(
 79 |     root_path: str, 
 80 |     max_depth: Optional[Union[int, str]] = None,
 81 |     include_symbols: Union[bool, str] = False,
 82 |     focus_dirs: Optional[List[str]] = None,
 83 |     max_symbols_per_file: Union[int, str] = 5
 84 | ) -> str:
 85 |     """
 86 |     🗺️ STEP 1: Map the codebase structure - start simple, then zoom in!
 87 |     
 88 |     PROGRESSIVE DISCOVERY WORKFLOW:
 89 |     1. First call: explore_repo("/path/to/project") - See directory structure only
 90 |     2. Zoom in: explore_repo("/path/to/project", focus_dirs=["src"], include_symbols=True)
 91 |     3. Go deeper: explore_repo("/path/to/project", max_depth=3, include_symbols=True)
 92 |     
 93 |     INPUTS:
 94 |     - root_path: The ABSOLUTE path to the project (e.g., "/Users/john/myproject")
 95 |                  NOT relative paths like "./myproject" or "~/myproject"
 96 |     - max_depth: How deep to traverse directories (None = unlimited, accepts int or string)
 97 |     - include_symbols: Show function/class signatures with docs (False = dirs only, accepts bool or string)
 98 |     - focus_dirs: List of top-level directories to focus on (e.g., ["src", "lib"])
 99 |     - max_symbols_per_file: Max symbols to show per file when include_symbols=True (accepts int or string)
100 |     
101 |     EXAMPLE 1 - Initial exploration (directory only):
102 |     explore_repo("/Users/john/project")
103 |     # Returns:
104 |     # /Users/john/project/
105 |     # ├── src/
106 |     # ├── tests/
107 |     # ├── docs/
108 |     # └── README.md
109 |     
110 |     EXAMPLE 2 - Zoom into src/ with symbols:
111 |     explore_repo("/Users/john/project", focus_dirs=["src"], include_symbols=True)
112 |     # Returns:
113 |     # /Users/john/project/
114 |     # └── src/
115 |     #     ├── auth.py
116 |     #     │   ├── class AuthService: # Handles user authentication
117 |     #     │   ├── def authenticate(username, password): # Validates credentials
118 |     #     │   └── def logout(session_id): # Ends user session
119 |     #     └── models.py
120 |     #         ├── class User(BaseModel): # User account model
121 |     #         └── ... and 3 more
122 |     
123 |     EXAMPLE 3 - Limited depth exploration:
124 |     explore_repo("/Users/john/project", max_depth=1, include_symbols=True)
125 |     # Shows only top-level dirs and files with their symbols
126 |     
127 |     💡 PRO TIP: Start with include_symbols=False to see structure, then set it to True
128 |     for areas you want to examine in detail. This prevents information overload!
129 |     
130 |     ⚡ PERFORMANCE: Symbol extraction is cached per git commit - subsequent calls are instant!
131 |     
132 |     WHAT TO DO NEXT:
133 |     - If you found interesting directories, zoom in with focus_dirs
134 |     - If you see relevant files, use find_symbol() to locate specific functions
135 |     """
136 |     try:
137 |         # Convert string inputs to proper types (for LLMs that pass strings)
138 |         if max_depth is not None and isinstance(max_depth, str):
139 |             max_depth = int(max_depth)
140 |         if isinstance(max_symbols_per_file, str):
141 |             max_symbols_per_file = int(max_symbols_per_file)
142 |         if isinstance(include_symbols, str):
143 |             include_symbols = include_symbols.lower() in ('true', '1', 'yes')
144 |             
145 |         indexer = get_indexer(root_path)
146 |         tree = indexer.explore_repo(
147 |             max_depth=max_depth,
148 |             include_symbols=include_symbols,
149 |             focus_dirs=focus_dirs,
150 |             max_symbols_per_file=max_symbols_per_file
151 |         )
152 |         return tree
153 |     except Exception as e:
154 |         return f"Error exploring repository: {str(e)}"
155 | 
156 | 
157 | @mcp.tool
158 | def find_symbol(root_path: str, query: str) -> List[Dict[str, Any]]:
159 |     """
160 |     🔍 STEP 2: Find specific functions, classes, or methods in the codebase.
161 |     
162 |     USE THIS AFTER explore_repo() when you need to locate a specific piece of code.
163 |     Uses fuzzy matching - you don't need the exact name!
164 |     
165 |     INPUTS:
166 |     - root_path: Same ABSOLUTE path used in explore_repo
167 |     - query: What you're looking for (fuzzy search works!)
168 |              Examples: "auth", "user service", "validate", "parseJSON"
169 |     
170 |     EXAMPLE INPUTS:
171 |     find_symbol("/Users/john/awesome-project", "authenticate")
172 |     find_symbol("/Users/john/awesome-project", "user model")  # Fuzzy matches "UserModel"
173 |     
174 |     EXAMPLE OUTPUT:
175 |     [
176 |         {
177 |             "name": "authenticate_user",
178 |             "type": "function",
179 |             "path": "/Users/john/awesome-project/src/auth.py",
180 |             "start_line": 45,
181 |             "end_line": 67
182 |         },
183 |         {
184 |             "name": "AuthService",
185 |             "type": "class", 
186 |             "path": "/Users/john/awesome-project/src/services.py",
187 |             "start_line": 12,
188 |             "end_line": 89
189 |         }
190 |     ]
191 |     
192 |     RETURNS:
193 |     List of symbol objects (dictionaries). Save these objects - you'll pass them to what_breaks()!
194 |     Empty list if no matches found.
195 |     
196 |     WHAT TO DO NEXT:
197 |     Pick a symbol from the results and pass THE ENTIRE SYMBOL OBJECT to what_breaks() 
198 |     to see where it's used in the codebase.
199 |     """
200 |     try:
201 |         indexer = get_indexer(root_path)
202 |         results = indexer.find_symbol(query)
203 |         return results
204 |     except Exception as e:
205 |         return [{"error": f"Error finding symbol: {str(e)}"}]
206 | 
207 | 
208 | @mcp.tool  
209 | def what_breaks(exact_symbol: Dict[str, Any]) -> Dict[str, Any]:
210 |     """
211 |     💥 STEP 3: See what code might break if you change this symbol.
212 |     
213 |     USE THIS AFTER find_symbol() to understand the impact of changing a function/class.
214 |     Shows you every place in the codebase where this symbol name appears.
215 |     
216 |     INPUT:
217 |     - exact_symbol: Pass THE ENTIRE SYMBOL OBJECT from find_symbol(), not just the name!
218 |                    Must be a dictionary with AT LEAST 'name' and 'path' keys.
219 |     
220 |     EXAMPLE INPUT:
221 |     # First, get a symbol from find_symbol():
222 |     symbols = find_symbol("/Users/john/project", "authenticate")
223 |     symbol = symbols[0]  # Pick the first result
224 |     
225 |     # Then pass THE WHOLE SYMBOL OBJECT:
226 |     what_breaks(symbol)
227 |     # or directly:
228 |     what_breaks({
229 |         "name": "authenticate_user",
230 |         "type": "function",
231 |         "path": "/Users/john/project/src/auth.py",
232 |         "start_line": 45,
233 |         "end_line": 67
234 |     })
235 |     
236 |     EXAMPLE OUTPUT:
237 |     {
238 |         "references": [
239 |             {
240 |                 "file": "/Users/john/project/src/api.py",
241 |                 "line": 23,
242 |                 "text": "    user = authenticate_user(username, password)"
243 |             },
244 |             {
245 |                 "file": "/Users/john/project/tests/test_auth.py", 
246 |                 "line": 45,
247 |                 "text": "def test_authenticate_user():"
248 |             }
249 |         ],
250 |         "total_count": 2,
251 |         "note": "Found 2 potential references based on a text search for the name 'authenticate_user'. This may include comments, strings, or other unrelated symbols."
252 |     }
253 |     
254 |     ⚠️ IMPORTANT: This does a text search for the name, so it might find:
255 |     - Actual function calls (what you want!)
256 |     - Comments mentioning the function
257 |     - Other functions/variables with the same name
258 |     - Strings containing the name
259 |     
260 |     Review each reference to determine if it's actually affected.
261 |     """
262 |     try:
263 |         # Extract root path from the symbol's path
264 |         symbol_path = Path(exact_symbol['path'])
265 |         root_path = str(symbol_path.parent)
266 |         
267 |         # Find a suitable root (go up until we find a git repo or reach root)
268 |         while root_path != '/':
269 |             if (Path(root_path) / '.git').exists():
270 |                 break
271 |             parent = Path(root_path).parent
272 |             if parent == Path(root_path):
273 |                 break
274 |             root_path = str(parent)
275 |         
276 |         indexer = get_indexer(root_path)
277 |         return indexer.what_breaks(exact_symbol)
278 |     except Exception as e:
279 |         return {"error": f"Error finding references: {str(e)}"}
280 | 
281 | 
282 | def main():
283 |     """Main entry point for the XRAY MCP server."""
284 |     mcp.run()
285 | 
286 | 
287 | if __name__ == "__main__":
288 |     main()
```

--------------------------------------------------------------------------------
/src/xray/core/indexer.py:
--------------------------------------------------------------------------------

```python
  1 | """Core indexing engine for XRAY - ast-grep based implementation."""
  2 | 
  3 | import os
  4 | import re
  5 | import ast
  6 | import json
  7 | import subprocess
  8 | import hashlib
  9 | import pickle
 10 | from pathlib import Path
 11 | from typing import Dict, List, Optional, Any, Set, Tuple
 12 | import fnmatch
 13 | from thefuzz import fuzz
 14 | 
 15 | # Default exclusions
 16 | DEFAULT_EXCLUSIONS = {
 17 |     # Directories
 18 |     "node_modules", "vendor", "__pycache__", "venv", ".venv", "env",
 19 |     "target", "build", "dist", ".git", ".svn", ".hg", ".idea", ".vscode",
 20 |     ".xray", "site-packages", ".tox", ".pytest_cache", ".mypy_cache",
 21 |     
 22 |     # File patterns
 23 |     "*.pyc", "*.pyo", "*.pyd", "*.so", "*.dll", "*.log", 
 24 |     ".DS_Store", "Thumbs.db", "*.swp", "*.swo", "*~"
 25 | }
 26 | 
 27 | # Language extensions
 28 | LANGUAGE_MAP = {
 29 |     ".py": "python",
 30 |     ".js": "javascript", 
 31 |     ".jsx": "javascript",
 32 |     ".mjs": "javascript",
 33 |     ".ts": "typescript",
 34 |     ".tsx": "typescript",
 35 |     ".go": "go",
 36 | }
 37 | 
 38 | 
 39 | class XRayIndexer:
 40 |     """Main indexer for XRAY - provides file tree and symbol extraction using ast-grep."""
 41 |     
 42 |     def __init__(self, root_path: str):
 43 |         self.root_path = Path(root_path).resolve()
 44 |         self._cache = {}
 45 |         self._init_cache()
 46 |     
 47 |     def _init_cache(self):
 48 |         """Initialize cache based on git commit SHA."""
 49 |         try:
 50 |             # Get current git commit SHA
 51 |             result = subprocess.run(
 52 |                 ["git", "rev-parse", "HEAD"],
 53 |                 cwd=self.root_path,
 54 |                 capture_output=True,
 55 |                 text=True
 56 |             )
 57 |             if result.returncode == 0:
 58 |                 self.commit_sha = result.stdout.strip()
 59 |                 self.cache_dir = Path(f"/tmp/.xray_cache/{self.commit_sha}")
 60 |                 self.cache_dir.mkdir(parents=True, exist_ok=True)
 61 |                 self._load_cache()
 62 |             else:
 63 |                 self.commit_sha = None
 64 |                 self.cache_dir = None
 65 |         except:
 66 |             self.commit_sha = None
 67 |             self.cache_dir = None
 68 |     
 69 |     def _load_cache(self):
 70 |         """Load cache from disk if available."""
 71 |         if not self.cache_dir:
 72 |             return
 73 |         
 74 |         cache_file = self.cache_dir / "symbols.pkl"
 75 |         if cache_file.exists():
 76 |             try:
 77 |                 with open(cache_file, 'rb') as f:
 78 |                     self._cache = pickle.load(f)
 79 |             except:
 80 |                 self._cache = {}
 81 |     
 82 |     def _save_cache(self):
 83 |         """Save cache to disk."""
 84 |         if not self.cache_dir:
 85 |             return
 86 |         
 87 |         cache_file = self.cache_dir / "symbols.pkl"
 88 |         try:
 89 |             with open(cache_file, 'wb') as f:
 90 |                 pickle.dump(self._cache, f)
 91 |         except:
 92 |             pass
 93 |     
 94 |     def _get_cache_key(self, file_path: Path) -> str:
 95 |         """Generate cache key for a file."""
 96 |         try:
 97 |             stat = file_path.stat()
 98 |             return f"{file_path}:{stat.st_mtime}:{stat.st_size}"
 99 |         except:
100 |             return str(file_path)
101 |     
102 |     def explore_repo(
103 |         self, 
104 |         max_depth: Optional[int] = None,
105 |         include_symbols: bool = False,
106 |         focus_dirs: Optional[List[str]] = None,
107 |         max_symbols_per_file: int = 5
108 |     ) -> str:
109 |         """
110 |         Build a visual file tree with optional symbol skeletons.
111 |         
112 |         Args:
113 |             max_depth: Limit directory traversal depth
114 |             include_symbols: Include symbol skeletons in output
115 |             focus_dirs: Only include these top-level directories
116 |             max_symbols_per_file: Max symbols to show per file
117 |             
118 |         Returns:
119 |             Formatted tree string
120 |         """
121 |         # Get gitignore patterns if available
122 |         gitignore_patterns = self._parse_gitignore()
123 |         
124 |         # Build the tree
125 |         tree_lines = []
126 |         self._build_tree_recursive_enhanced(
127 |             self.root_path, 
128 |             tree_lines, 
129 |             "", 
130 |             gitignore_patterns,
131 |             current_depth=0,
132 |             max_depth=max_depth,
133 |             include_symbols=include_symbols,
134 |             focus_dirs=focus_dirs,
135 |             max_symbols_per_file=max_symbols_per_file,
136 |             is_last=True
137 |         )
138 |         
139 |         # Save cache after building tree
140 |         if include_symbols:
141 |             self._save_cache()
142 |         
143 |         return "\n".join(tree_lines)
144 |     
145 |     def _parse_gitignore(self) -> Set[str]:
146 |         """Parse .gitignore file if it exists."""
147 |         patterns = set()
148 |         gitignore_path = self.root_path / ".gitignore"
149 |         
150 |         if gitignore_path.exists():
151 |             try:
152 |                 with open(gitignore_path, 'r', encoding='utf-8') as f:
153 |                     for line in f:
154 |                         line = line.strip()
155 |                         if line and not line.startswith('#'):
156 |                             patterns.add(line)
157 |             except Exception:
158 |                 pass
159 |         
160 |         return patterns
161 |     
162 |     def _should_exclude(self, path: Path, gitignore_patterns: Set[str]) -> bool:
163 |         """Check if a path should be excluded."""
164 |         name = path.name
165 |         
166 |         # Check default exclusions
167 |         if name in DEFAULT_EXCLUSIONS:
168 |             return True
169 |         
170 |         # Check file pattern exclusions
171 |         for pattern in DEFAULT_EXCLUSIONS:
172 |             if '*' in pattern and fnmatch.fnmatch(name, pattern):
173 |                 return True
174 |         
175 |         # Check gitignore patterns (simplified)
176 |         for pattern in gitignore_patterns:
177 |             if pattern in str(path.relative_to(self.root_path)):
178 |                 return True
179 |             if fnmatch.fnmatch(name, pattern):
180 |                 return True
181 |         
182 |         return False
183 |     
184 |     def _should_include_dir(self, path: Path, focus_dirs: Optional[List[str]], current_depth: int) -> bool:
185 |         """Check if a directory should be included based on focus_dirs."""
186 |         if not focus_dirs or current_depth > 0:
187 |             return True
188 |         
189 |         # At depth 0 (top-level), only include if in focus_dirs
190 |         return path.name in focus_dirs
191 |     
192 |     def _build_tree_recursive_enhanced(
193 |         self, 
194 |         path: Path, 
195 |         tree_lines: List[str], 
196 |         prefix: str, 
197 |         gitignore_patterns: Set[str],
198 |         current_depth: int,
199 |         max_depth: Optional[int],
200 |         include_symbols: bool,
201 |         focus_dirs: Optional[List[str]],
202 |         max_symbols_per_file: int,
203 |         is_last: bool = False
204 |     ):
205 |         """Recursively build the tree representation with enhanced features."""
206 |         if self._should_exclude(path, gitignore_patterns):
207 |             return
208 |         
209 |         # Check depth limit
210 |         if max_depth is not None and current_depth > max_depth:
211 |             return
212 |         
213 |         # Check focus_dirs for directories
214 |         if path.is_dir() and not self._should_include_dir(path, focus_dirs, current_depth):
215 |             return
216 |         
217 |         # Add current item
218 |         name = path.name if path != self.root_path else str(path)
219 |         connector = "└── " if is_last else "├── "
220 |         
221 |         # For files, add skeleton if requested
222 |         if path.is_file() and include_symbols and path.suffix.lower() in LANGUAGE_MAP:
223 |             skeleton = self._get_file_skeleton_enhanced(path, max_symbols_per_file)
224 |             if skeleton:
225 |                 # Format with indented skeleton
226 |                 if path == self.root_path:
227 |                     tree_lines.append(name)
228 |                 else:
229 |                     tree_lines.append(prefix + connector + name)
230 |                 
231 |                 # Add skeleton lines
232 |                 for i, skel_line in enumerate(skeleton):
233 |                     is_last_skel = (i == len(skeleton) - 1)
234 |                     skel_prefix = prefix + ("    " if is_last else "│   ")
235 |                     skel_connector = "└── " if is_last_skel else "├── "
236 |                     tree_lines.append(skel_prefix + skel_connector + skel_line)
237 |             else:
238 |                 # No skeleton, just show filename
239 |                 if path == self.root_path:
240 |                     tree_lines.append(name)
241 |                 else:
242 |                     tree_lines.append(prefix + connector + name)
243 |         else:
244 |             # Directory or file without symbols
245 |             if path == self.root_path:
246 |                 tree_lines.append(name)
247 |             else:
248 |                 tree_lines.append(prefix + connector + name)
249 |         
250 |         # Only recurse into directories
251 |         if path.is_dir():
252 |             # Get children and sort them
253 |             try:
254 |                 children = sorted(path.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower()))
255 |                 # Filter out excluded items
256 |                 children = [c for c in children if not self._should_exclude(c, gitignore_patterns)]
257 |                 
258 |                 # Apply focus_dirs filter at top level
259 |                 if current_depth == 0 and focus_dirs:
260 |                     children = [c for c in children if c.is_file() or c.name in focus_dirs]
261 |                 
262 |                 for i, child in enumerate(children):
263 |                     is_last_child = (i == len(children) - 1)
264 |                     extension = "    " if is_last else "│   "
265 |                     new_prefix = prefix + extension if path != self.root_path else ""
266 |                     
267 |                     self._build_tree_recursive_enhanced(
268 |                         child, 
269 |                         tree_lines, 
270 |                         new_prefix, 
271 |                         gitignore_patterns,
272 |                         current_depth + 1,
273 |                         max_depth,
274 |                         include_symbols,
275 |                         focus_dirs,
276 |                         max_symbols_per_file,
277 |                         is_last_child
278 |                     )
279 |             except PermissionError:
280 |                 pass
281 |     
282 |     def _get_file_skeleton_enhanced(self, file_path: Path, max_symbols: int) -> List[str]:
283 |         """Extract enhanced symbol info including signatures and docstrings."""
284 |         # Check cache first
285 |         cache_key = self._get_cache_key(file_path)
286 |         if cache_key in self._cache:
287 |             cached_symbols = self._cache[cache_key]
288 |             return self._format_enhanced_skeleton(cached_symbols, max_symbols)
289 |         
290 |         language = LANGUAGE_MAP.get(file_path.suffix.lower())
291 |         if not language:
292 |             return []
293 |         
294 |         try:
295 |             with open(file_path, 'r', encoding='utf-8') as f:
296 |                 content = f.read()
297 |             
298 |             if language == "python":
299 |                 symbols = self._extract_python_symbols_enhanced(content)
300 |             else:
301 |                 symbols = self._extract_regex_symbols_enhanced(content, language)
302 |             
303 |             # Cache the results
304 |             self._cache[cache_key] = symbols
305 |             
306 |             return self._format_enhanced_skeleton(symbols, max_symbols)
307 |         
308 |         except Exception:
309 |             return []
310 |     
311 |     def _format_enhanced_skeleton(self, symbols: List[Dict[str, str]], max_symbols: int) -> List[str]:
312 |         """Format enhanced symbol info for display."""
313 |         if not symbols:
314 |             return []
315 |         
316 |         lines = []
317 |         shown_count = min(len(symbols), max_symbols)
318 |         
319 |         for symbol in symbols[:shown_count]:
320 |             line = symbol['signature']
321 |             if symbol.get('doc'):
322 |                 line += f" # {symbol['doc']}"
323 |             lines.append(line)
324 |         
325 |         if len(symbols) > max_symbols:
326 |             remaining = len(symbols) - max_symbols
327 |             lines.append(f"... and {remaining} more")
328 |         
329 |         return lines
330 |     
331 |     def _extract_python_symbols_enhanced(self, content: str) -> List[Dict[str, str]]:
332 |         """Extract Python symbols with signatures and docstrings."""
333 |         symbols = []
334 |         try:
335 |             tree = ast.parse(content)
336 |             for node in ast.iter_child_nodes(tree):
337 |                 if isinstance(node, ast.ClassDef):
338 |                     sig = f"class {node.name}"
339 |                     if node.bases:
340 |                         base_names = []
341 |                         for base in node.bases:
342 |                             if isinstance(base, ast.Name):
343 |                                 base_names.append(base.id)
344 |                             elif isinstance(base, ast.Attribute):
345 |                                 base_names.append(ast.unparse(base))
346 |                         if base_names:
347 |                             sig += f"({', '.join(base_names)})"
348 |                     sig += ":"
349 |                     
350 |                     doc = ast.get_docstring(node)
351 |                     if doc:
352 |                         doc = doc.split('\n')[0].strip()[:50]
353 |                     
354 |                     symbols.append({'signature': sig, 'doc': doc or ''})
355 |                     
356 |                 elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
357 |                     # Build function signature
358 |                     sig = "async def " if isinstance(node, ast.AsyncFunctionDef) else "def "
359 |                     sig += f"{node.name}("
360 |                     
361 |                     # Add parameters
362 |                     args = []
363 |                     for arg in node.args.args:
364 |                         args.append(arg.arg)
365 |                     if args:
366 |                         sig += ", ".join(args)
367 |                     sig += "):"
368 |                     
369 |                     doc = ast.get_docstring(node)
370 |                     if doc:
371 |                         doc = doc.split('\n')[0].strip()[:50]
372 |                     
373 |                     symbols.append({'signature': sig, 'doc': doc or ''})
374 |         except:
375 |             pass
376 |         return symbols
377 |     
378 |     def _extract_regex_symbols_enhanced(self, content: str, language: str) -> List[Dict[str, str]]:
379 |         """Extract symbols with signatures and comments for JS/TS/Go."""
380 |         symbols = []
381 |         
382 |         # Language-specific patterns
383 |         if language in ["javascript", "typescript"]:
384 |             patterns = [
385 |                 # Function with preceding comment
386 |                 (r'(?://\s*(.+?)\n)?^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\((.*?)\)', 
387 |                  lambda m: {'signature': f"function {m.group(2)}({m.group(3)}):", 'doc': (m.group(1) or '').strip()}),
388 |                 
389 |                 # Class with preceding comment
390 |                 (r'(?://\s*(.+?)\n)?^\s*(?:export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?', 
391 |                  lambda m: {'signature': f"class {m.group(2)}" + (f" extends {m.group(3)}" if m.group(3) else "") + ":", 
392 |                            'doc': (m.group(1) or '').strip()}),
393 |                 
394 |                 # Arrow function with const
395 |                 (r'(?://\s*(.+?)\n)?^\s*(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s*)?\((.*?)\)\s*=>', 
396 |                  lambda m: {'signature': f"const {m.group(2)} = ({m.group(3)}) =>", 'doc': (m.group(1) or '').strip()}),
397 |             ]
398 |         elif language == "go":
399 |             patterns = [
400 |                 # Function with preceding comment
401 |                 (r'(?://\s*(.+?)\n)?^func\s+(\w+)\s*\((.*?)\)', 
402 |                  lambda m: {'signature': f"func {m.group(2)}({m.group(3)})", 'doc': (m.group(1) or '').strip()}),
403 |                 
404 |                 # Method with preceding comment
405 |                 (r'(?://\s*(.+?)\n)?^func\s*\((\w+\s+[*]?\w+)\)\s*(\w+)\s*\((.*?)\)', 
406 |                  lambda m: {'signature': f"func ({m.group(2)}) {m.group(3)}({m.group(4)})", 
407 |                            'doc': (m.group(1) or '').strip()}),
408 |                 
409 |                 # Type struct with preceding comment
410 |                 (r'(?://\s*(.+?)\n)?^type\s+(\w+)\s+struct', 
411 |                  lambda m: {'signature': f"type {m.group(2)} struct", 'doc': (m.group(1) or '').strip()}),
412 |             ]
413 |         else:
414 |             return symbols
415 |         
416 |         # Apply patterns
417 |         for pattern, extractor in patterns:
418 |             for match in re.finditer(pattern, content, re.MULTILINE):
419 |                 symbols.append(extractor(match))
420 |         
421 |         return symbols
422 |     
423 |     def find_symbol(self, query: str, limit: int = 10) -> List[Dict[str, Any]]:
424 |         """
425 |         Find symbols matching the query using fuzzy search.
426 |         Uses ast-grep to find all symbols, then fuzzy matches against the query.
427 |         
428 |         Returns a list of the top matching "Exact Symbol" objects.
429 |         """
430 |         all_symbols = []
431 |         
432 |         # Define patterns for different symbol types
433 |         patterns = [
434 |             # Python functions and classes
435 |             ("def $NAME($$$):", "function"),
436 |             ("class $NAME($$$):", "class"),
437 |             ("async def $NAME($$$):", "function"),
438 |             
439 |             # JavaScript/TypeScript functions and classes
440 |             ("function $NAME($$$)", "function"),
441 |             ("const $NAME = ($$$) =>", "function"),
442 |             ("let $NAME = ($$$) =>", "function"),
443 |             ("var $NAME = ($$$) =>", "function"),
444 |             ("class $NAME", "class"),
445 |             ("interface $NAME", "interface"),
446 |             ("type $NAME =", "type"),
447 |             
448 |             # Go functions and types
449 |             ("func $NAME($$$)", "function"),
450 |             ("func ($$$) $NAME($$$)", "method"),
451 |             ("type $NAME struct", "struct"),
452 |             ("type $NAME interface", "interface"),
453 |         ]
454 |         
455 |         # Run ast-grep for each pattern
456 |         for pattern, symbol_type in patterns:
457 |             cmd = [
458 |                 "ast-grep",
459 |                 "--pattern", pattern,
460 |                 "--json",
461 |                 str(self.root_path)
462 |             ]
463 |             
464 |             result = subprocess.run(cmd, capture_output=True, text=True)
465 |             
466 |             if result.returncode == 0:
467 |                 try:
468 |                     matches = json.loads(result.stdout)
469 |                     for match in matches:
470 |                         # Extract details from match
471 |                         text = match.get("text", "")
472 |                         file_path = match.get("file", "")
473 |                         start = match.get("range", {}).get("start", {})
474 |                         end = match.get("range", {}).get("end", {})
475 |                         
476 |                         # Extract the name from metavariables
477 |                         metavars = match.get("metaVariables", {})
478 |                         name = None
479 |                         
480 |                         # Try to get NAME from metavariables
481 |                         if "NAME" in metavars:
482 |                             name = metavars["NAME"]["text"]
483 |                         else:
484 |                             # Fallback to regex extraction
485 |                             name = self._extract_symbol_name(text)
486 |                         
487 |                         if name:
488 |                             symbol = {
489 |                                 "name": name,
490 |                                 "type": symbol_type,
491 |                                 "path": file_path,
492 |                                 "start_line": start.get("line", 1),
493 |                                 "end_line": end.get("line", start.get("line", 1))
494 |                             }
495 |                             all_symbols.append(symbol)
496 |                 except json.JSONDecodeError:
497 |                     continue
498 |         
499 |         # Deduplicate symbols (same name and location)
500 |         seen = set()
501 |         unique_symbols = []
502 |         for symbol in all_symbols:
503 |             key = (symbol["name"], symbol["path"], symbol["start_line"])
504 |             if key not in seen:
505 |                 seen.add(key)
506 |                 unique_symbols.append(symbol)
507 |         
508 |         # Now perform fuzzy matching against the query
509 |         scored_symbols = []
510 |         for symbol in unique_symbols:
511 |             # Calculate similarity score
512 |             score = fuzz.partial_ratio(query.lower(), symbol["name"].lower())
513 |             
514 |             # Boost score for exact substring matches
515 |             if query.lower() in symbol["name"].lower():
516 |                 score = max(score, 80)
517 |             
518 |             scored_symbols.append((score, symbol))
519 |         
520 |         # Sort by score and take top results
521 |         scored_symbols.sort(key=lambda x: x[0], reverse=True)
522 |         top_symbols = [s[1] for s in scored_symbols[:limit]]
523 |         
524 |         return top_symbols
525 |     
526 |     def _extract_symbol_name(self, text: str) -> Optional[str]:
527 |         """Extract the symbol name from matched text."""
528 |         # Patterns to extract names from different definition types
529 |         patterns = [
530 |             r'(?:def|class|function|interface|type)\s+(\w+)',
531 |             r'(?:const|let|var)\s+(\w+)\s*=',
532 |             r'func\s+(?:\([^)]+\)\s+)?(\w+)',
533 |         ]
534 |         
535 |         for pattern in patterns:
536 |             match = re.search(pattern, text)
537 |             if match:
538 |                 return match.group(1)
539 |         
540 |         return None
541 |     
542 |     def what_breaks(self, exact_symbol: Dict[str, Any]) -> Dict[str, Any]:
543 |         """
544 |         Find what uses a symbol (reverse dependencies).
545 |         Simplified to use basic text search for speed and simplicity.
546 |         
547 |         Returns a dictionary with references and a standard caveat.
548 |         """
549 |         symbol_name = exact_symbol['name']
550 |         references = []
551 |         
552 |         # Use simple grep-like search for the symbol name
553 |         # Check if ripgrep is available, otherwise fall back to Python
554 |         try:
555 |             # Try using ripgrep if available
556 |             cmd = [
557 |                 "rg",
558 |                 "-w",  # whole word
559 |                 "--json",
560 |                 symbol_name,
561 |                 str(self.root_path)
562 |             ]
563 |             
564 |             result = subprocess.run(cmd, capture_output=True, text=True)
565 |             
566 |             if result.returncode == 0:
567 |                 # Parse ripgrep JSON output
568 |                 for line in result.stdout.strip().split('\n'):
569 |                     if line:
570 |                         try:
571 |                             data = json.loads(line)
572 |                             if data.get("type") == "match":
573 |                                 match_data = data.get("data", {})
574 |                                 references.append({
575 |                                     "file": match_data.get("path", {}).get("text", ""),
576 |                                     "line": match_data.get("line_number", 0),
577 |                                     "text": match_data.get("lines", {}).get("text", "").strip()
578 |                                 })
579 |                         except json.JSONDecodeError:
580 |                             continue
581 |             else:
582 |                 # Ripgrep not available or failed, fall back to Python
583 |                 references = self._python_text_search(symbol_name)
584 |         except FileNotFoundError:
585 |             # Ripgrep not installed, use Python fallback
586 |             references = self._python_text_search(symbol_name)
587 |         
588 |         return {
589 |             "references": references,
590 |             "total_count": len(references),
591 |             "note": f"Found {len(references)} potential references based on a text search for the name '{symbol_name}'. This may include comments, strings, or other unrelated symbols."
592 |         }
593 |     
594 |     def _python_text_search(self, symbol_name: str) -> List[Dict[str, Any]]:
595 |         """Fallback text search using Python when ripgrep is not available."""
596 |         references = []
597 |         gitignore_patterns = self._parse_gitignore()
598 |         
599 |         # Create word boundary pattern
600 |         pattern = re.compile(r'\b' + re.escape(symbol_name) + r'\b')
601 |         
602 |         for file_path in self.root_path.rglob('*'):
603 |             if not file_path.is_file():
604 |                 continue
605 |             
606 |             # Skip excluded files
607 |             if self._should_exclude(file_path, gitignore_patterns):
608 |                 continue
609 |             
610 |             # Only search in source files
611 |             if file_path.suffix.lower() not in LANGUAGE_MAP:
612 |                 continue
613 |             
614 |             try:
615 |                 with open(file_path, 'r', encoding='utf-8') as f:
616 |                     for line_num, line in enumerate(f, 1):
617 |                         if pattern.search(line):
618 |                             references.append({
619 |                                 "file": str(file_path),
620 |                                 "line": line_num,
621 |                                 "text": line.strip()
622 |                             })
623 |             except Exception:
624 |                 continue
625 |         
626 |         return references
```