# 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 | [](https://python.org) [](https://modelcontextprotocol.io) [](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
```