├── .gitignore ├── .python-version ├── README.md ├── code_analysis.py ├── docs └── images │ ├── integrations.png │ ├── mcptools.png │ ├── prompt.png │ └── tools.png ├── pyproject.toml └── uv.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .aider* 2 | 3 | # Python-generated files 4 | __pycache__/ 5 | *.py[oc] 6 | build/ 7 | dist/ 8 | wheels/ 9 | *.egg-info/ 10 | *.so 11 | 12 | # Virtual environments 13 | .venv/ 14 | venv/ 15 | ENV/ 16 | .aider*/ 17 | .env 18 | .python-version 19 | 20 | # Development environments 21 | .idea/ 22 | .vscode/ 23 | *.swp 24 | *.swo 25 | .DS_Store 26 | *.sublime-* 27 | .settings/ 28 | .project 29 | .pydevproject 30 | 31 | # Testing 32 | .coverage 33 | htmlcov/ 34 | .pytest_cache/ 35 | .tox/ 36 | .hypothesis/ 37 | .nox/ 38 | 39 | # Distribution 40 | *.egg 41 | *.whl 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Logs and databases 46 | *.log 47 | *.sqlite 48 | *.db 49 | 50 | # Cache directories 51 | .mypy_cache/ 52 | .ruff_cache/ 53 | .pytest_cache/ 54 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Code Analysis MCP Server 2 | 3 | A Model Context Protocol (MCP) server that enables AI models to understand and analyze codebases through natural language conversations. 4 | 5 | ## ✨ Highlights 6 | 7 | - **Natural Code Exploration**: Ask high-level questions about your codebase 8 | ``` 9 | "What are all the different payment providers integrated in the system?" 10 | ``` 11 | 12 | - **Deep Code Understanding**: Extract insights about data models and system architecture 13 | ``` 14 | "How does the user authentication flow work from frontend to database?" 15 | ``` 16 | 17 | - **Dynamic Analysis**: Trace data flows and understand system relationships 18 | ``` 19 | "Show me all the places where we calculate transaction fees" 20 | ``` 21 | 22 | ## Limitations 23 | 24 | This tool is a simpler alternative to more sophisticated code analysis tools / copilot like [Aider](https://aider.chat/). While it lacks the advanced code analysis capabilities and robustness of tools like Aider, it offers a lightweight solution for codebase exploration if you already have a Claude Pro subscription. The main advantages are: 25 | 26 | - **Cost-Effective**: Using your existing Claude Pro subscription means no additional API costs, unlike tools that can get expensive when analyzing large codebases 27 | - **Simple Setup**: Quick to get started with minimal configuration 28 | - **Basic Analysis**: Good for high-level code understanding and exploration 29 | 30 | Note that due to its simpler approach to code analysis, it may make more errors or provide less detailed insights compared to more specialized tools. 31 | 32 | ## 🚀 Quick Start 33 | 34 | 1. Install the server: 35 | ```bash 36 | git clone https://github.com/saiprashanths/code-analysis-mcp.git 37 | cd code-analysis-mcp 38 | ``` 39 | 40 | 2. Install [Claude Desktop App](https://claude.ai/download). For more instructions on setting up an MCP Server with Claude Desktop app, [see docs](https://modelcontextprotocol.io/quickstart/user) 41 | 42 | 3. Configure the server in Claude Desktop's configuration file: 43 | - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 44 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json` 45 | 46 | ```json 47 | { 48 | "mcpServers": { 49 | "code-analysis": { 50 | "command": "uv", 51 | "args": [ 52 | "--directory", 53 | "/PATH/TO/YOUR/REPO", 54 | "run", 55 | "code_analysis.py" 56 | ] 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | 4. Restart Claude Desktop and start analyzing your code! 63 | 64 | 5. Ensure the tool is correctly setup 65 | - Clicking the tools icon should display the `code-analysis` tools 66 | 67 | 68 | 69 | - Clicking the integrations button should display the `analyze_code_repository` prompt 70 | 71 | 72 | 73 | ## 💡 Example Usage 74 | 75 | Here's how you might use the tool to understand a complex codebase: 76 | 77 | Start by clicking on the `analyze_code_repository` prompt shown below. That will guide you through the process 78 | to initialize the repo and get started. 79 | 80 | 81 | 82 | ```text 83 | You: How does the transaction processing work in this system? 84 | 85 | Claude: Let me analyze the data flow for transactions. 86 | Looking at the codebase, I can see that transaction processing happens through several key components: 87 | 88 | 1. Raw transaction data is captured in the `TransactionEvent` model with fields for: 89 | - Payment status 90 | - Amount 91 | - Fee calculations 92 | - Processing timestamps 93 | 94 | 2. This data is then processed through the PaymentSummary model, which: 95 | - Validates payment provider responses 96 | - Calculates processing fees 97 | - Records transaction status 98 | 99 | Would you like me to explain any part of this in more detail? 100 | 101 | You: What are the key database tables involved? 102 | 103 | Claude: The main tables in the transaction processing pipeline are: 104 | 105 | 1. transaction_events: Stores raw payment events 106 | 2. payment_summary: Processed transaction records 107 | 3. payment_providers: Provider configuration 108 | 4. merchant_accounts: Account-specific settings 109 | 110 | Let me know if you'd like to see the schema for any of these tables! 111 | ``` 112 | 113 | ## 🔧 Available Tools 114 | 115 | ### initialize_repository 116 | Initialize a code repository for analysis: 117 | ```python 118 | initialize_repository("/path/to/repo") 119 | # Output: Successfully initialized code repository at: /path/to/repo 120 | ``` 121 | 122 | ### get_repo_info 123 | Retrieve information about the initialized repository: 124 | ```python 125 | get_repo_info() 126 | # Output: 127 | # Code Repository Information: 128 | # Path: /path/to/repo 129 | # Exists: True 130 | # Is Directory: True 131 | # Found .gitignore file 132 | ``` 133 | 134 | ### get_repo_structure 135 | Examine repository file structure: 136 | ```python 137 | get_repo_structure(depth=2) 138 | # Output: 139 | # 📁 src/ 140 | # 📁 api/ 141 | # 📄 routes.py 142 | # 📄 models.py 143 | # 📁 utils/ 144 | # 📄 helpers.py 145 | # 📄 main.py 146 | ``` 147 | 148 | ### read_file 149 | Read and analyze specific files: 150 | ```python 151 | read_file("src/api/models.py") 152 | # Output: 153 | # File: src/api/models.py 154 | # Language: python 155 | # Size: 2.3 KB 156 | # 157 | # [File contents...] 158 | ``` 159 | 160 | ## ⚙️ Technical Details 161 | 162 | - Default scanning depth: 3 levels 163 | - Maximum file size: 1MB 164 | - Maximum lines per file: 1000 165 | - Ignores paths listed in .gitignore 166 | - Local file system access only 167 | - Validates file paths to prevent directory traversal 168 | 169 | ## 📝 License 170 | 171 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 172 | -------------------------------------------------------------------------------- /code_analysis.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Optional, List, Dict, Union, Set 3 | from dataclasses import dataclass 4 | from mcp.server.fastmcp import FastMCP 5 | from mcp.server.fastmcp.prompts.base import UserMessage, AssistantMessage 6 | import os 7 | from pathspec import PathSpec 8 | from pathspec.patterns import GitWildMatchPattern 9 | 10 | @dataclass 11 | class Summary: 12 | file_count: int = 0 13 | dir_count: int = 0 14 | total_size: int = 0 15 | 16 | @dataclass 17 | class FileStructure: 18 | path: str 19 | type: str 20 | size: Optional[int] = None 21 | children: Optional[List['FileStructure']] = None 22 | summary: Optional[Summary] = None 23 | 24 | class RepoStructureAnalyzer: 25 | def __init__(self, repo_path: Path, max_depth: int = 3, max_children: int = 100): 26 | self.repo_path = repo_path 27 | self.MAX_DEPTH = max_depth 28 | self.MAX_CHILDREN = max_children 29 | 30 | # Default patterns to always ignore 31 | self.default_ignore_patterns = ['.git', '__pycache__', 'node_modules'] 32 | 33 | # Load .gitignore if it exists 34 | self.gitignore_spec = self._load_gitignore() 35 | 36 | def _load_gitignore(self) -> Optional[PathSpec]: 37 | """Load .gitignore patterns if the file exists.""" 38 | gitignore_path = self.repo_path / '.gitignore' 39 | patterns = [] 40 | 41 | # Add default patterns 42 | for pattern in self.default_ignore_patterns: 43 | patterns.append(GitWildMatchPattern(pattern)) 44 | 45 | # Add patterns from .gitignore if it exists 46 | if gitignore_path.exists() and gitignore_path.is_file(): 47 | try: 48 | with open(gitignore_path, 'r') as f: 49 | for line in f: 50 | line = line.strip() 51 | # Skip empty lines and comments 52 | if line and not line.startswith('#'): 53 | patterns.append(GitWildMatchPattern(line)) 54 | except Exception as e: 55 | print(f"Error reading .gitignore: {e}") 56 | 57 | return PathSpec(patterns) 58 | 59 | def _is_safe_path(self, path: Path) -> bool: 60 | """Check if the given path is safe (no directory traversal).""" 61 | try: 62 | return path.resolve().is_relative_to(self.repo_path.resolve()) 63 | except ValueError: 64 | return False 65 | 66 | def format_structure(self, structure: FileStructure) -> str: 67 | """Format the file structure into a readable string.""" 68 | output = [] 69 | 70 | def format_size(size: int) -> str: 71 | units = ['B', 'KB', 'MB', 'GB'] 72 | value = float(size) 73 | index = 0 74 | while value >= 1024 and index < len(units) - 1: 75 | value /= 1024 76 | index += 1 77 | return f"{value:.1f} {units[index]}" 78 | 79 | def format_item(item: FileStructure, level: int = 0) -> None: 80 | indent = ' ' * level 81 | 82 | if item.type == 'directory': 83 | output.append(f"{indent}📁 {item.path}/") 84 | 85 | if item.summary: 86 | summary = item.summary 87 | output.append( 88 | f"{indent} Contains: {summary.file_count} files, " 89 | f"{summary.dir_count} directories, " 90 | f"{format_size(summary.total_size)}" 91 | ) 92 | elif item.children: 93 | for child in item.children: 94 | format_item(child, level + 1) 95 | else: 96 | output.append(f"{indent}📄 {item.path} ({format_size(item.size or 0)})") 97 | 98 | format_item(structure) 99 | return '\n'.join(output) 100 | 101 | def should_ignore(self, path: str) -> bool: 102 | """Check if path should be ignored based on gitignore patterns.""" 103 | return self.gitignore_spec.match_file(path) 104 | 105 | def get_structure( 106 | self, 107 | current_path: Path, 108 | relative_path: str = '', 109 | current_depth: int = 0, 110 | max_depth: Optional[int] = None 111 | ) -> FileStructure: 112 | """Recursively get the structure of files and directories.""" 113 | if max_depth is None: 114 | max_depth = self.MAX_DEPTH 115 | 116 | # Verify path safety 117 | if not self._is_safe_path(current_path): 118 | raise ValueError(f"Invalid path: {current_path}") 119 | 120 | # Skip symbolic links 121 | if current_path.is_symlink(): 122 | raise ValueError(f"Symbolic links are not supported: {current_path}") 123 | 124 | stats = current_path.stat() 125 | rel_path = relative_path or current_path.name 126 | 127 | if relative_path and self.should_ignore(relative_path): 128 | raise ValueError(f"Path {relative_path} is ignored") 129 | 130 | if current_path.is_file(): 131 | return FileStructure( 132 | path=rel_path, 133 | type="file", 134 | size=stats.st_size 135 | ) 136 | 137 | if current_path.is_dir(): 138 | children = [] 139 | summary = Summary() 140 | 141 | if current_depth < max_depth: 142 | try: 143 | entries = list(current_path.iterdir()) 144 | 145 | for entry in entries: 146 | if len(children) >= self.MAX_CHILDREN: 147 | if entry.is_file() and not entry.is_symlink(): 148 | summary.file_count += 1 149 | summary.total_size += entry.stat().st_size 150 | elif entry.is_dir() and not entry.is_symlink(): 151 | summary.dir_count += 1 152 | continue 153 | 154 | entry_relative_path = str(entry.relative_to(self.repo_path)) 155 | 156 | try: 157 | if self.should_ignore(entry_relative_path): 158 | continue 159 | 160 | if entry.is_symlink(): 161 | continue 162 | 163 | entry_stats = entry.stat() 164 | 165 | if entry.is_file(): 166 | summary.file_count += 1 167 | summary.total_size += entry_stats.st_size 168 | elif entry.is_dir(): 169 | summary.dir_count += 1 170 | 171 | child = self.get_structure( 172 | entry, 173 | entry_relative_path, 174 | current_depth + 1, 175 | max_depth 176 | ) 177 | children.append(child) 178 | 179 | except Exception as error: 180 | print(f"Error processing {entry}: {error}") 181 | continue 182 | 183 | except Exception as error: 184 | print(f"Error reading directory {current_path}: {error}") 185 | 186 | return FileStructure( 187 | path=rel_path, 188 | type="directory", 189 | children=children if children else None, 190 | summary=summary if current_depth >= max_depth or len(children) >= self.MAX_CHILDREN else None 191 | ) 192 | 193 | raise ValueError(f"Unsupported file type at {current_path}") 194 | 195 | class FileReader: 196 | def __init__(self, repo_path: Path): 197 | self.repo_path = repo_path 198 | self.MAX_SIZE = 1024 * 1024 # 1MB 199 | self.MAX_LINES = 1000 # Maximum number of lines to return 200 | 201 | def _detect_language(self, file_path: str) -> str: 202 | """Detect the programming language based on file extension.""" 203 | ext = Path(file_path).suffix.lower() 204 | 205 | # Extensive mapping of file extensions to languages 206 | language_map = { 207 | # Programming Languages 208 | '.py': 'python', 209 | '.js': 'javascript', 210 | '.jsx': 'javascript', 211 | '.ts': 'typescript', 212 | '.tsx': 'typescript', 213 | '.java': 'java', 214 | '.cpp': 'cpp', 215 | '.cc': 'cpp', 216 | '.hpp': 'cpp', 217 | '.c': 'c', 218 | '.h': 'c', 219 | '.cs': 'csharp', 220 | '.rb': 'ruby', 221 | '.php': 'php', 222 | '.go': 'go', 223 | '.rs': 'rust', 224 | '.swift': 'swift', 225 | '.kt': 'kotlin', 226 | '.scala': 'scala', 227 | '.m': 'objective-c', 228 | '.mm': 'objective-c', 229 | 230 | # Web Technologies 231 | '.html': 'html', 232 | '.htm': 'html', 233 | '.css': 'css', 234 | '.scss': 'scss', 235 | '.sass': 'scss', 236 | '.less': 'less', 237 | '.vue': 'vue', 238 | '.svelte': 'svelte', 239 | 240 | # Data & Config Files 241 | '.json': 'json', 242 | '.xml': 'xml', 243 | '.yaml': 'yaml', 244 | '.yml': 'yaml', 245 | '.toml': 'toml', 246 | '.ini': 'ini', 247 | '.conf': 'config', 248 | 249 | # Documentation 250 | '.md': 'markdown', 251 | '.markdown': 'markdown', 252 | '.rst': 'restructuredtext', 253 | '.tex': 'latex', 254 | 255 | # Shell Scripts 256 | '.sh': 'shell', 257 | '.bash': 'shell', 258 | '.zsh': 'shell', 259 | '.fish': 'shell', 260 | '.bat': 'batch', 261 | '.cmd': 'batch', 262 | '.ps1': 'powershell', 263 | 264 | # Other Common Types 265 | '.sql': 'sql', 266 | '.r': 'r', 267 | '.gradle': 'gradle', 268 | '.dockerfile': 'dockerfile', 269 | '.env': 'env', 270 | '.gitignore': 'gitignore' 271 | } 272 | 273 | # Handle files without extension but specific names 274 | if not ext: 275 | filename = Path(file_path).name.lower() 276 | name_map = { 277 | 'dockerfile': 'dockerfile', 278 | 'makefile': 'makefile', 279 | 'jenkinsfile': 'jenkinsfile', 280 | 'vagrantfile': 'ruby', 281 | '.env': 'env', 282 | '.gitignore': 'gitignore' 283 | } 284 | return name_map.get(filename, 'text') 285 | 286 | return language_map.get(ext, 'text') 287 | 288 | def read_file(self, file_path: str) -> Dict[str, Union[List[Dict[str, str]], bool]]: 289 | """Read and format file contents for LLM consumption.""" 290 | try: 291 | full_path = self.repo_path / file_path 292 | 293 | # Check if path is safe 294 | if not full_path.resolve().is_relative_to(self.repo_path.resolve()): 295 | return { 296 | "content": [{ 297 | "type": "text", 298 | "text": f"Error: Attempted to access file outside repository: {file_path}" 299 | }], 300 | "isError": True 301 | } 302 | 303 | # Check if file exists 304 | if not full_path.exists(): 305 | return { 306 | "content": [{ 307 | "type": "text", 308 | "text": f"File {file_path} not found" 309 | }], 310 | "isError": True 311 | } 312 | 313 | # Check if it's a symbolic link 314 | if full_path.is_symlink(): 315 | return { 316 | "content": [{ 317 | "type": "text", 318 | "text": f"Error: Symbolic links are not supported: {file_path}" 319 | }], 320 | "isError": True 321 | } 322 | 323 | # Get file stats 324 | stats = full_path.stat() 325 | 326 | # Check file size 327 | if stats.st_size > self.MAX_SIZE: 328 | return { 329 | "content": [{ 330 | "type": "text", 331 | "text": f"File {file_path} is too large ({stats.st_size} bytes). " 332 | f"Maximum size is {self.MAX_SIZE} bytes." 333 | }], 334 | "isError": True 335 | } 336 | 337 | # Read file content with line limit 338 | lines = [] 339 | line_count = 0 340 | truncated = False 341 | 342 | with open(full_path, 'r', encoding='utf-8') as f: 343 | for line in f: 344 | line_count += 1 345 | if line_count <= self.MAX_LINES: 346 | lines.append(line.rstrip('\n')) 347 | else: 348 | truncated = True 349 | break 350 | 351 | content = '\n'.join(lines) 352 | if truncated: 353 | content += f"\n\n[File truncated after {self.MAX_LINES} lines]" 354 | 355 | # Detect language 356 | language = self._detect_language(file_path) 357 | 358 | return { 359 | "content": [{ 360 | "type": "text", 361 | "text": f"File: {file_path}\n" 362 | f"Language: {language}\n" 363 | f"Size: {stats.st_size} bytes\n" 364 | f"Total lines: {line_count}\n\n" 365 | f"{content}" 366 | }] 367 | } 368 | 369 | except UnicodeDecodeError: 370 | return { 371 | "content": [{ 372 | "type": "text", 373 | "text": f"Error: File {file_path} appears to be a binary file" 374 | }], 375 | "isError": True 376 | } 377 | except Exception as error: 378 | return { 379 | "content": [{ 380 | "type": "text", 381 | "text": f"Error reading file: {str(error)}" 382 | }], 383 | "isError": True 384 | } 385 | 386 | class CodeAnalysisServer(FastMCP): 387 | def __init__(self, name: str): 388 | # super().__init__(name) 389 | # First, call the parent class constructor with capabilities 390 | super().__init__( 391 | name, 392 | capabilities={ 393 | "prompts": {} # Enable prompts capability 394 | } 395 | ) 396 | self.repo_path: Optional[Path] = None 397 | self.analyzer: Optional[RepoStructureAnalyzer] = None 398 | self.file_reader: Optional[FileReader] = None 399 | 400 | # Add prompts capability 401 | # self.capabilities["prompts"] = {} 402 | 403 | def initialize_repo(self, path: str) -> None: 404 | """Initialize the repository path and analysis tools.""" 405 | if not path or path in (".", "./"): 406 | raise ValueError("Repository path must be an absolute path") 407 | 408 | repo_path = Path(path).resolve() 409 | if not repo_path.is_absolute(): 410 | raise ValueError(f"Repository path must be absolute, got: {repo_path}") 411 | if not repo_path.exists(): 412 | raise ValueError(f"Repository path does not exist: {repo_path}") 413 | if not repo_path.is_dir(): 414 | raise ValueError(f"Repository path is not a directory: {repo_path}") 415 | 416 | self.repo_path = repo_path 417 | self.analyzer = RepoStructureAnalyzer(self.repo_path) 418 | self.file_reader = FileReader(self.repo_path) 419 | 420 | # Initialize server 421 | mcp = CodeAnalysisServer("code-analysis") 422 | 423 | @mcp.tool() 424 | async def initialize_repository(path: str) -> str: 425 | """Initialize the repository path for future code analysis operations. 426 | 427 | Args: 428 | path: Path to the repository root directory that contains the code to analyze 429 | """ 430 | try: 431 | mcp.initialize_repo(path) 432 | gitignore_path = Path(path) / '.gitignore' 433 | gitignore_status = "Found .gitignore file" if gitignore_path.exists() else "No .gitignore file present" 434 | return f"Successfully initialized code repository at: {mcp.repo_path}\n{gitignore_status}" 435 | except ValueError as e: 436 | return f"Error initializing code repository: {str(e)}" 437 | 438 | @mcp.tool() 439 | async def get_repo_info() -> str: 440 | """Get information about the currently initialized code repository.""" 441 | if not mcp.repo_path: 442 | return "No code repository has been initialized yet. Please use initialize_repository first." 443 | 444 | gitignore_path = mcp.repo_path / '.gitignore' 445 | gitignore_status = "Found .gitignore file" if gitignore_path.exists() else "No .gitignore file present" 446 | 447 | return f"""Code Repository Information: 448 | Path: {mcp.repo_path} 449 | Exists: {mcp.repo_path.exists()} 450 | Is Directory: {mcp.repo_path.is_dir()} 451 | {gitignore_status}""" 452 | 453 | @mcp.tool() 454 | async def get_repo_structure(sub_path: Optional[str] = None, depth: Optional[int] = None) -> str: 455 | """Get the structure of files and directories in the repository. 456 | 457 | Args: 458 | sub_path: Optional subdirectory path relative to repository root 459 | depth: Optional maximum depth to traverse (default is 3) 460 | """ 461 | if not mcp.repo_path or not mcp.analyzer: 462 | return "No code repository has been initialized yet. Please use initialize_repository first." 463 | 464 | try: 465 | target_path = mcp.repo_path 466 | if sub_path: 467 | target_path = mcp.repo_path / sub_path 468 | if not mcp.analyzer._is_safe_path(target_path): 469 | return "Error: Invalid path - directory traversal not allowed" 470 | 471 | structure = mcp.analyzer.get_structure( 472 | target_path, 473 | sub_path or '', 474 | max_depth=depth 475 | ) 476 | return mcp.analyzer.format_structure(structure) 477 | except Exception as e: 478 | return f"Error analyzing repository structure: {str(e)}" 479 | 480 | @mcp.tool() 481 | async def read_file(file_path: str) -> str: 482 | """Read and display the contents of a file from the repository. 483 | 484 | Args: 485 | file_path: Path to the file relative to repository root 486 | """ 487 | if not mcp.repo_path or not mcp.file_reader or not mcp.analyzer: 488 | return "No code repository has been initialized yet. Please use initialize_repository first." 489 | 490 | try: 491 | # Check if file should be ignored based on gitignore patterns 492 | if mcp.analyzer.should_ignore(file_path): 493 | return f"File {file_path} is ignored based on .gitignore patterns" 494 | 495 | result = mcp.file_reader.read_file(file_path) 496 | 497 | if result.get("isError", False): 498 | return result["content"][0]["text"] 499 | 500 | return result["content"][0]["text"] 501 | 502 | except Exception as e: 503 | return f"Error reading file: {str(e)}" 504 | 505 | @mcp.prompt() 506 | def analyze_code_repository(codebase_path: str) -> list[UserMessage | AssistantMessage]: 507 | """Analyze a code repository at the specified path. 508 | 509 | Args: 510 | codebase_path: Absolute path to the code repository 511 | """ 512 | return [ 513 | UserMessage(f"""You are an AI assistant specialized in codebase analysis, operating as part of an MCP server named code-analysis. Your task is to analyze codebases and answer user questions about them using a set of specialized tools. 514 | 515 | The codebase we are going to analyze is located at {codebase_path} 516 | The user will ask specific questions about this codebase. To answer the user's questions, follow these steps: 517 | 518 | 1. Initialize Repository: 519 | - Use the `initialize_repository(path: str) -> str` tool with the full path to the repository root directory. 520 | - This step is required before using any other tools. 521 | 522 | 2. Verify Initialization: 523 | - Use the `get_repo_info() -> str` tool to confirm successful initialization. 524 | - This will show the path, existence verification, and .gitignore status. 525 | 526 | 3. Get Repository Structure: 527 | - Use the `get_repo_structure(sub_path?: str, depth?: int) -> str` tool to generate a tree view of the repository's file structure. 528 | - Start with the default depth for an overview, then use sub_path to explore specific directories of interest. 529 | - Increase depth only for detailed investigation of specific areas. 530 | 531 | 4. Read Files: 532 | - Use the `read_file(file_path: str) -> str` tool to read and display file contents with syntax recognition. 533 | - This tool is limited to files under 1MB and 1000 lines. 534 | - Start with README files and other documentation to gain initial context. 535 | 536 | 5. Systematic Investigation: 537 | - Generate an initial hypothesis about the system based on the repository structure and documentation. 538 | - Use the tools strategically to explore the codebase, focusing on areas relevant to the user's question. 539 | - Continuously update your understanding as you gather more information. 540 | - Use the `memory` and/or `sequential-thinking` MCP servers if available 541 | 542 | 6. Evidence-Based Analysis: 543 | - Support all claims with concrete evidence from the codebase. 544 | - Clearly distinguish between directly verified code, inferred patterns, and areas requiring further investigation. 545 | 546 | 7. Comprehensive Analysis Presentation: 547 | Present your findings in the following format: 548 | a. Initial System Hypothesis 549 | b. Investigation Methodology 550 | c. Discovered System Characteristics 551 | d. Supporting Evidence 552 | e. Remaining Uncertainties 553 | f. Final Answer to User's Question 554 | 555 | Throughout your analysis, document your thought process inside tags. For each step: 556 | - State the current focus or question you're addressing. 557 | - List the potential tools you could use and explain your choice. 558 | - Document the results of each tool use, quoting relevant code snippets or file contents. 559 | - Explain your reasoning when forming hypotheses or drawing conclusions. 560 | - Summarize your findings periodically throughout the investigation. 561 | 562 | Be sure to use the available tools appropriately and document any limitations or errors encountered. It's okay for this section to be quite long. 563 | 564 | Remember: 565 | - You are operating in the context of an MCP server named code-analysis. 566 | - Always use the tools provided and do not assume access to any other capabilities. 567 | - If you encounter any errors or limitations with the tools, clearly state them in your analysis. 568 | - Maintain a systematic and evidence-based approach throughout your investigation. 569 | 570 | Now, you are ready to begin your analysis of the codebase. Please do the necessary steps to initialize. 571 | Let me know when you are ready and I will provide the question I want to investigate. 572 | """), 573 | # AssistantMessage("""I'll help you analyze this codebase. First, let me initialize the repository to get started. I will then present 574 | # my initial insights and ask you the question that you want to investigate. 575 | # """) 576 | ] 577 | 578 | if __name__ == "__main__": 579 | # Initialize and run the server 580 | mcp.run(transport='stdio') -------------------------------------------------------------------------------- /docs/images/integrations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saiprashanths/code-analysis-mcp/e0f047b71d4c8a5091a98b39675bdf1b90e72d33/docs/images/integrations.png -------------------------------------------------------------------------------- /docs/images/mcptools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saiprashanths/code-analysis-mcp/e0f047b71d4c8a5091a98b39675bdf1b90e72d33/docs/images/mcptools.png -------------------------------------------------------------------------------- /docs/images/prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saiprashanths/code-analysis-mcp/e0f047b71d4c8a5091a98b39675bdf1b90e72d33/docs/images/prompt.png -------------------------------------------------------------------------------- /docs/images/tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saiprashanths/code-analysis-mcp/e0f047b71d4c8a5091a98b39675bdf1b90e72d33/docs/images/tools.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "code-analysis-mcp" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "httpx>=0.28.1", 9 | "mcp[cli]>=1.2.1", 10 | "pathspec>=0.12.1", 11 | ] 12 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.12" 3 | 4 | [[package]] 5 | name = "annotated-types" 6 | version = "0.7.0" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 11 | ] 12 | 13 | [[package]] 14 | name = "anyio" 15 | version = "4.8.0" 16 | source = { registry = "https://pypi.org/simple" } 17 | dependencies = [ 18 | { name = "idna" }, 19 | { name = "sniffio" }, 20 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 21 | ] 22 | sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } 23 | wheels = [ 24 | { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, 25 | ] 26 | 27 | [[package]] 28 | name = "certifi" 29 | version = "2025.1.31" 30 | source = { registry = "https://pypi.org/simple" } 31 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 32 | wheels = [ 33 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 34 | ] 35 | 36 | [[package]] 37 | name = "click" 38 | version = "8.1.8" 39 | source = { registry = "https://pypi.org/simple" } 40 | dependencies = [ 41 | { name = "colorama", marker = "sys_platform == 'win32'" }, 42 | ] 43 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 44 | wheels = [ 45 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 46 | ] 47 | 48 | [[package]] 49 | name = "code-analysis-mcp" 50 | version = "0.1.0" 51 | source = { virtual = "." } 52 | dependencies = [ 53 | { name = "httpx" }, 54 | { name = "mcp", extra = ["cli"] }, 55 | { name = "pathspec" }, 56 | ] 57 | 58 | [package.metadata] 59 | requires-dist = [ 60 | { name = "httpx", specifier = ">=0.28.1" }, 61 | { name = "mcp", extras = ["cli"], specifier = ">=1.2.1" }, 62 | { name = "pathspec", specifier = ">=0.12.1" }, 63 | ] 64 | 65 | [[package]] 66 | name = "colorama" 67 | version = "0.4.6" 68 | source = { registry = "https://pypi.org/simple" } 69 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 70 | wheels = [ 71 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 72 | ] 73 | 74 | [[package]] 75 | name = "h11" 76 | version = "0.14.0" 77 | source = { registry = "https://pypi.org/simple" } 78 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 79 | wheels = [ 80 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 81 | ] 82 | 83 | [[package]] 84 | name = "httpcore" 85 | version = "1.0.7" 86 | source = { registry = "https://pypi.org/simple" } 87 | dependencies = [ 88 | { name = "certifi" }, 89 | { name = "h11" }, 90 | ] 91 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 92 | wheels = [ 93 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 94 | ] 95 | 96 | [[package]] 97 | name = "httpx" 98 | version = "0.28.1" 99 | source = { registry = "https://pypi.org/simple" } 100 | dependencies = [ 101 | { name = "anyio" }, 102 | { name = "certifi" }, 103 | { name = "httpcore" }, 104 | { name = "idna" }, 105 | ] 106 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 107 | wheels = [ 108 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 109 | ] 110 | 111 | [[package]] 112 | name = "httpx-sse" 113 | version = "0.4.0" 114 | source = { registry = "https://pypi.org/simple" } 115 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 116 | wheels = [ 117 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 118 | ] 119 | 120 | [[package]] 121 | name = "idna" 122 | version = "3.10" 123 | source = { registry = "https://pypi.org/simple" } 124 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 125 | wheels = [ 126 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 127 | ] 128 | 129 | [[package]] 130 | name = "markdown-it-py" 131 | version = "3.0.0" 132 | source = { registry = "https://pypi.org/simple" } 133 | dependencies = [ 134 | { name = "mdurl" }, 135 | ] 136 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } 137 | wheels = [ 138 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, 139 | ] 140 | 141 | [[package]] 142 | name = "mcp" 143 | version = "1.2.1" 144 | source = { registry = "https://pypi.org/simple" } 145 | dependencies = [ 146 | { name = "anyio" }, 147 | { name = "httpx" }, 148 | { name = "httpx-sse" }, 149 | { name = "pydantic" }, 150 | { name = "pydantic-settings" }, 151 | { name = "sse-starlette" }, 152 | { name = "starlette" }, 153 | { name = "uvicorn" }, 154 | ] 155 | sdist = { url = "https://files.pythonhosted.org/packages/fc/30/51e4555826126e3954fa2ab1e934bf74163c5fe05e98f38ca4d0f8abbf63/mcp-1.2.1.tar.gz", hash = "sha256:c9d43dbfe943aa1530e2be8f54b73af3ebfb071243827b4483d421684806cb45", size = 103968 } 156 | wheels = [ 157 | { url = "https://files.pythonhosted.org/packages/4c/0d/6770742a84c8aa1d36c0d628896a380584c5759612e66af7446af07d8775/mcp-1.2.1-py3-none-any.whl", hash = "sha256:579bf9c9157850ebb1344f3ca6f7a3021b0123c44c9f089ef577a7062522f0fd", size = 66453 }, 158 | ] 159 | 160 | [package.optional-dependencies] 161 | cli = [ 162 | { name = "python-dotenv" }, 163 | { name = "typer" }, 164 | ] 165 | 166 | [[package]] 167 | name = "mdurl" 168 | version = "0.1.2" 169 | source = { registry = "https://pypi.org/simple" } 170 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 171 | wheels = [ 172 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, 173 | ] 174 | 175 | [[package]] 176 | name = "pathspec" 177 | version = "0.12.1" 178 | source = { registry = "https://pypi.org/simple" } 179 | sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } 180 | wheels = [ 181 | { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, 182 | ] 183 | 184 | [[package]] 185 | name = "pydantic" 186 | version = "2.10.6" 187 | source = { registry = "https://pypi.org/simple" } 188 | dependencies = [ 189 | { name = "annotated-types" }, 190 | { name = "pydantic-core" }, 191 | { name = "typing-extensions" }, 192 | ] 193 | sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } 194 | wheels = [ 195 | { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, 196 | ] 197 | 198 | [[package]] 199 | name = "pydantic-core" 200 | version = "2.27.2" 201 | source = { registry = "https://pypi.org/simple" } 202 | dependencies = [ 203 | { name = "typing-extensions" }, 204 | ] 205 | sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } 206 | wheels = [ 207 | { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, 208 | { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, 209 | { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, 210 | { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, 211 | { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, 212 | { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, 213 | { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, 214 | { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, 215 | { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, 216 | { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, 217 | { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, 218 | { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, 219 | { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, 220 | { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, 221 | { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, 222 | { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, 223 | { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, 224 | { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, 225 | { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, 226 | { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, 227 | { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, 228 | { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, 229 | { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, 230 | { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, 231 | { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, 232 | { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, 233 | { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, 234 | { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, 235 | ] 236 | 237 | [[package]] 238 | name = "pydantic-settings" 239 | version = "2.7.1" 240 | source = { registry = "https://pypi.org/simple" } 241 | dependencies = [ 242 | { name = "pydantic" }, 243 | { name = "python-dotenv" }, 244 | ] 245 | sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 } 246 | wheels = [ 247 | { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 }, 248 | ] 249 | 250 | [[package]] 251 | name = "pygments" 252 | version = "2.19.1" 253 | source = { registry = "https://pypi.org/simple" } 254 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } 255 | wheels = [ 256 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, 257 | ] 258 | 259 | [[package]] 260 | name = "python-dotenv" 261 | version = "1.0.1" 262 | source = { registry = "https://pypi.org/simple" } 263 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 264 | wheels = [ 265 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 266 | ] 267 | 268 | [[package]] 269 | name = "rich" 270 | version = "13.9.4" 271 | source = { registry = "https://pypi.org/simple" } 272 | dependencies = [ 273 | { name = "markdown-it-py" }, 274 | { name = "pygments" }, 275 | ] 276 | sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } 277 | wheels = [ 278 | { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, 279 | ] 280 | 281 | [[package]] 282 | name = "shellingham" 283 | version = "1.5.4" 284 | source = { registry = "https://pypi.org/simple" } 285 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } 286 | wheels = [ 287 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, 288 | ] 289 | 290 | [[package]] 291 | name = "sniffio" 292 | version = "1.3.1" 293 | source = { registry = "https://pypi.org/simple" } 294 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 295 | wheels = [ 296 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 297 | ] 298 | 299 | [[package]] 300 | name = "sse-starlette" 301 | version = "2.2.1" 302 | source = { registry = "https://pypi.org/simple" } 303 | dependencies = [ 304 | { name = "anyio" }, 305 | { name = "starlette" }, 306 | ] 307 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 308 | wheels = [ 309 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 310 | ] 311 | 312 | [[package]] 313 | name = "starlette" 314 | version = "0.45.3" 315 | source = { registry = "https://pypi.org/simple" } 316 | dependencies = [ 317 | { name = "anyio" }, 318 | ] 319 | sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 } 320 | wheels = [ 321 | { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 }, 322 | ] 323 | 324 | [[package]] 325 | name = "typer" 326 | version = "0.15.1" 327 | source = { registry = "https://pypi.org/simple" } 328 | dependencies = [ 329 | { name = "click" }, 330 | { name = "rich" }, 331 | { name = "shellingham" }, 332 | { name = "typing-extensions" }, 333 | ] 334 | sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 } 335 | wheels = [ 336 | { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, 337 | ] 338 | 339 | [[package]] 340 | name = "typing-extensions" 341 | version = "4.12.2" 342 | source = { registry = "https://pypi.org/simple" } 343 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 344 | wheels = [ 345 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 346 | ] 347 | 348 | [[package]] 349 | name = "uvicorn" 350 | version = "0.34.0" 351 | source = { registry = "https://pypi.org/simple" } 352 | dependencies = [ 353 | { name = "click" }, 354 | { name = "h11" }, 355 | ] 356 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } 357 | wheels = [ 358 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, 359 | ] 360 | --------------------------------------------------------------------------------