mcp-server-analyzer 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ """MCP Python Analyzer - A Model Context Protocol server for Python code analysis."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "MCP Python Analyzer Team"
5
+ __email__ = "contact@example.com"
6
+
7
+ __all__ = ["__author__", "__email__", "__version__"]
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+ """Main entry point for the MCP Python Analyzer server."""
3
+
4
+ from mcp_python_analyzer.server import main
5
+
6
+ if __name__ == "__main__":
7
+ main()
@@ -0,0 +1,6 @@
1
+ """Analyzers package for RUFF and VULTURE integration."""
2
+
3
+ from .ruff import RuffAnalyzer
4
+ from .vulture import VultureAnalyzer
5
+
6
+ __all__ = ["RuffAnalyzer", "VultureAnalyzer"]
@@ -0,0 +1,188 @@
1
+ """RUFF integration for Python code linting and formatting."""
2
+
3
+ import json
4
+ import subprocess
5
+ import tempfile
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from mcp_python_analyzer.models import RuffCheckResult, RuffFormatResult, RuffIssue
10
+
11
+
12
+ class RuffAnalyzer:
13
+ """Handles RUFF-based Python code analysis."""
14
+
15
+ def __init__(self) -> None:
16
+ """Initialize the RUFF analyzer."""
17
+ self._check_ruff_installation()
18
+
19
+ def _check_ruff_installation(self) -> None:
20
+ """Verify that RUFF is installed and accessible."""
21
+ try:
22
+ result = subprocess.run(
23
+ ["ruff", "--version"],
24
+ capture_output=True,
25
+ text=True,
26
+ check=True,
27
+ timeout=10,
28
+ )
29
+ if result.returncode != 0:
30
+ raise RuntimeError("RUFF is not properly installed")
31
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
32
+ raise RuntimeError(f"RUFF is not available: {e}") from e
33
+
34
+ def check_code(self, code: str, config_path: str | None = None) -> RuffCheckResult:
35
+ """
36
+ Run RUFF linter on the provided code.
37
+
38
+ Args:
39
+ code: Python code to lint
40
+ config_path: Optional path to RUFF configuration file
41
+
42
+ Returns:
43
+ RuffCheckResult containing linting issues
44
+ """
45
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
46
+ f.write(code)
47
+ temp_file = Path(f.name)
48
+
49
+ try:
50
+ cmd = ["ruff", "check", "--output-format=json", str(temp_file)]
51
+ if config_path:
52
+ cmd.extend(["--config", config_path])
53
+
54
+ result = subprocess.run(
55
+ cmd, capture_output=True, text=True, timeout=30, check=False
56
+ )
57
+
58
+ # RUFF returns non-zero exit code when issues are found
59
+ if result.returncode not in (0, 1):
60
+ raise RuntimeError(f"RUFF check failed: {result.stderr}")
61
+
62
+ issues: list[RuffIssue] = []
63
+ if result.stdout.strip():
64
+ try:
65
+ ruff_output = json.loads(result.stdout)
66
+ for item in ruff_output:
67
+ end_location: dict[str, int] = item.get("end_location") or {}
68
+ fix_info: dict[str, Any] = item.get("fix") or {}
69
+ issue = RuffIssue(
70
+ line=item["location"]["row"],
71
+ column=item["location"]["column"],
72
+ end_line=end_location.get("row"),
73
+ end_column=end_location.get("column"),
74
+ rule=item["code"],
75
+ message=item["message"],
76
+ severity=self._get_severity(item["code"]),
77
+ fixable=fix_info.get("applicability") == "safe",
78
+ )
79
+ issues.append(issue)
80
+ except json.JSONDecodeError as e:
81
+ raise RuntimeError(f"Failed to parse RUFF output: {e}") from e
82
+
83
+ return RuffCheckResult(
84
+ issues=issues,
85
+ total_issues=len(issues),
86
+ fixable_issues=sum(1 for issue in issues if issue.fixable),
87
+ )
88
+
89
+ finally:
90
+ temp_file.unlink()
91
+
92
+ def check_code_for_ci(
93
+ self, code: str, output_format: str = "json", config_path: str | None = None
94
+ ) -> str:
95
+ """
96
+ Run RUFF linter with specific output format for CI/CD systems.
97
+
98
+ Args:
99
+ code: Python code to lint
100
+ output_format: Output format (json, gitlab, github, sarif)
101
+ config_path: Optional path to RUFF configuration file
102
+
103
+ Returns:
104
+ Raw RUFF output in specified format
105
+ """
106
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
107
+ f.write(code)
108
+ temp_file = Path(f.name)
109
+
110
+ try:
111
+ cmd = ["ruff", "check", f"--output-format={output_format}", str(temp_file)]
112
+ if config_path:
113
+ cmd.extend(["--config", config_path])
114
+
115
+ result = subprocess.run(
116
+ cmd, capture_output=True, text=True, timeout=30, check=False
117
+ )
118
+
119
+ # RUFF returns non-zero exit code when issues are found
120
+ if result.returncode not in (0, 1):
121
+ raise RuntimeError(f"RUFF check failed: {result.stderr}")
122
+
123
+ return result.stdout
124
+
125
+ finally:
126
+ temp_file.unlink()
127
+
128
+ def format_code(
129
+ self, code: str, config_path: str | None = None
130
+ ) -> RuffFormatResult:
131
+ """
132
+ Format Python code using RUFF formatter.
133
+
134
+ Args:
135
+ code: Python code to format
136
+ config_path: Optional path to RUFF configuration file
137
+
138
+ Returns:
139
+ RuffFormatResult containing formatted code
140
+ """
141
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
142
+ f.write(code)
143
+ temp_file = Path(f.name)
144
+
145
+ try:
146
+ cmd = ["ruff", "format", "--stdin-filename", str(temp_file)]
147
+ if config_path:
148
+ cmd.extend(["--config", config_path])
149
+
150
+ result = subprocess.run(
151
+ cmd, input=code, capture_output=True, text=True, timeout=30, check=False
152
+ )
153
+
154
+ if result.returncode != 0:
155
+ raise RuntimeError(f"RUFF format failed: {result.stderr}")
156
+
157
+ formatted_code = result.stdout
158
+ changed = formatted_code != code
159
+
160
+ return RuffFormatResult(
161
+ formatted_code=formatted_code,
162
+ changed=changed,
163
+ )
164
+
165
+ finally:
166
+ temp_file.unlink()
167
+
168
+ def _get_severity(self, rule_code: str) -> str:
169
+ """
170
+ Determine severity level based on RUFF rule code.
171
+
172
+ Args:
173
+ rule_code: RUFF rule code (e.g., F401, E302)
174
+
175
+ Returns:
176
+ Severity level string
177
+ """
178
+ # Map RUFF rule prefixes to severity levels
179
+ if rule_code.startswith(("F", "E9")):
180
+ return "error"
181
+ elif rule_code.startswith(("W", "E", "C90", "N", "B", "A", "C4")): # noqa: RET505
182
+ return "warning"
183
+ elif rule_code.startswith(
184
+ ("I", "UP", "PIE", "T20", "PT", "RET", "SIM", "TID", "ARG", "PL", "RUF")
185
+ ):
186
+ return "info"
187
+ else:
188
+ return "warning"
@@ -0,0 +1,194 @@
1
+ """VULTURE integration for Python dead code detection."""
2
+
3
+ import re
4
+ import subprocess
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ from mcp_python_analyzer.models import VultureItem, VultureScanResult
9
+
10
+
11
+ class VultureAnalyzer:
12
+ """Handles VULTURE-based dead code detection."""
13
+
14
+ def __init__(self) -> None:
15
+ """Initialize the VULTURE analyzer."""
16
+ self._check_vulture_installation()
17
+
18
+ def _check_vulture_installation(self) -> None:
19
+ """Verify that VULTURE is installed and accessible."""
20
+ try:
21
+ result = subprocess.run(
22
+ ["vulture", "--version"],
23
+ capture_output=True,
24
+ text=True,
25
+ timeout=10,
26
+ check=True,
27
+ )
28
+ if result.returncode != 0:
29
+ raise RuntimeError(
30
+ f"VULTURE is not properly installed: {result.stderr}"
31
+ )
32
+ except (
33
+ subprocess.CalledProcessError,
34
+ FileNotFoundError,
35
+ subprocess.TimeoutExpired,
36
+ ) as e:
37
+ raise RuntimeError(f"VULTURE is not available: {e}") from e
38
+
39
+ def scan_code(self, code: str, min_confidence: int = 80) -> VultureScanResult:
40
+ """
41
+ Scan Python code for dead/unused code using VULTURE.
42
+
43
+ Args:
44
+ code: Python code to analyze
45
+ min_confidence: Minimum confidence level (0-100) for reporting items
46
+
47
+ Returns:
48
+ VultureScanResult containing unused code items
49
+ """
50
+ if not 0 <= min_confidence <= 100: # noqa: PLR2004
51
+ raise ValueError("min_confidence must be between 0 and 100")
52
+
53
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
54
+ f.write(code)
55
+ temp_file = Path(f.name)
56
+
57
+ try:
58
+ cmd = [
59
+ "vulture",
60
+ str(temp_file),
61
+ f"--min-confidence={min_confidence}",
62
+ "--sort-by-size",
63
+ ]
64
+
65
+ # Check if pyproject.toml exists and use it
66
+ pyproject_path = Path.cwd() / "pyproject.toml"
67
+ if pyproject_path.exists():
68
+ cmd.extend(["--config", str(pyproject_path)])
69
+
70
+ result = subprocess.run(
71
+ cmd, capture_output=True, text=True, timeout=30, check=False
72
+ )
73
+
74
+ # VULTURE exit codes:
75
+ # 0: No dead code found
76
+ # 1: Invalid input (file missing, syntax error, wrong encoding)
77
+ # 2: Invalid command line arguments
78
+ # 3: Dead code found
79
+ if result.returncode not in (0, 1, 3):
80
+ error_msg = (
81
+ result.stderr.strip()
82
+ or f"Unknown VULTURE error (exit code {result.returncode})"
83
+ )
84
+ raise RuntimeError(f"VULTURE scan failed: {error_msg}")
85
+
86
+ items = []
87
+ if result.stdout.strip():
88
+ items = self._parse_vulture_output(result.stdout, str(temp_file))
89
+ high_confidence_percentage = 80
90
+ high_confidence_count = sum(
91
+ 1 for item in items if item.confidence >= high_confidence_percentage
92
+ )
93
+
94
+ return VultureScanResult(
95
+ unused_items=items,
96
+ total_items=len(items),
97
+ high_confidence_items=high_confidence_count,
98
+ )
99
+
100
+ except subprocess.TimeoutExpired:
101
+ raise RuntimeError("VULTURE scan timed out") from None
102
+ except (FileNotFoundError, PermissionError) as e:
103
+ raise RuntimeError(f"Failed to run VULTURE: {e}") from e
104
+ finally:
105
+ temp_file.unlink()
106
+
107
+ def _parse_vulture_output(
108
+ self, output: str, temp_filename: str
109
+ ) -> list[VultureItem]:
110
+ """
111
+ Parse VULTURE output into structured items.
112
+
113
+ Args:
114
+ output: Raw VULTURE output
115
+ temp_filename: Temporary file name to filter out from output
116
+
117
+ Returns:
118
+ List of VultureItem objects
119
+ """
120
+ items: list[VultureItem] = []
121
+ # VULTURE output format: filename:line: message (confidence%, size info)
122
+ pattern = (
123
+ r"^(.+):(\d+):\s+(.+?)\s+\((\d+)%\s+confidence(?:,\s+\d+\s+lines?)?\)$"
124
+ )
125
+
126
+ # Resolve the temp filename for comparison
127
+ temp_path_resolved = str(Path(temp_filename).resolve())
128
+
129
+ for line in output.strip().split("\n"):
130
+ if not line.strip():
131
+ continue
132
+
133
+ match = re.match(pattern, line)
134
+ if match:
135
+ filename, line_num, message, confidence = match.groups()
136
+
137
+ # Compare resolved paths to handle /private prefix on macOS
138
+ file_path_resolved = str(Path(filename).resolve())
139
+ if file_path_resolved != temp_path_resolved:
140
+ continue
141
+
142
+ # Extract item name and type from message
143
+ item_name, item_type = self._extract_item_info(message)
144
+
145
+ item = VultureItem(
146
+ name=item_name,
147
+ type=item_type,
148
+ line=int(line_num),
149
+ column=0, # Vulture doesn't provide column info in this format
150
+ confidence=int(confidence),
151
+ message=message,
152
+ )
153
+ items.append(item)
154
+
155
+ return items
156
+
157
+ def _extract_item_info(self, message: str) -> tuple[str, str]:
158
+ """
159
+ Extract item name and type from VULTURE message.
160
+
161
+ Args:
162
+ message: VULTURE message string
163
+
164
+ Returns:
165
+ Tuple of (item_name, item_type)
166
+ """
167
+ # Common VULTURE message patterns
168
+ patterns = [
169
+ (r"unused import '(.+?)'", "import"),
170
+ (r"unused function '(.+?)'", "function"),
171
+ (r"unused method '(.+?)'", "method"),
172
+ (r"unused class '(.+?)'", "class"),
173
+ (r"unused variable '(.+?)'", "variable"),
174
+ (r"unused attribute '(.+?)'", "attribute"),
175
+ (r"unused property '(.+?)'", "property"),
176
+ (r"unused argument '(.+?)'", "argument"),
177
+ ]
178
+
179
+ for pattern, item_type in patterns:
180
+ match = re.search(pattern, message, re.IGNORECASE)
181
+ if match:
182
+ return match.group(1), item_type
183
+
184
+ # Fallback: try to extract quoted name
185
+ quoted_match = re.search(r"'(.+?)'", message)
186
+ if quoted_match:
187
+ return quoted_match.group(1), "unknown"
188
+
189
+ # Last resort: use first word as name
190
+ words = message.split()
191
+ if words:
192
+ return words[0], "unknown"
193
+
194
+ return "unknown", "unknown"
@@ -0,0 +1,72 @@
1
+ """Pydantic models for MCP Python Analyzer responses and configurations."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class RuffIssue(BaseModel):
7
+ """Represents a RUFF linting issue."""
8
+
9
+ line: int = Field(description="Line number where the issue occurs")
10
+ column: int = Field(description="Column number where the issue occurs")
11
+ end_line: int | None = Field(
12
+ None, description="End line number for multi-line issues"
13
+ )
14
+ end_column: int | None = Field(
15
+ None, description="End column number for multi-line issues"
16
+ )
17
+ rule: str = Field(description="RUFF rule code (e.g., F401, E302)")
18
+ message: str = Field(description="Human-readable description of the issue")
19
+ severity: str = Field(description="Issue severity level")
20
+ fixable: bool = Field(False, description="Whether the issue can be auto-fixed")
21
+
22
+
23
+ class RuffCheckResult(BaseModel):
24
+ """Result of RUFF check operation."""
25
+
26
+ issues: list[RuffIssue] = Field(description="List of linting issues found")
27
+ total_issues: int = Field(description="Total number of issues")
28
+ fixable_issues: int = Field(description="Number of auto-fixable issues")
29
+
30
+
31
+ class RuffFormatResult(BaseModel):
32
+ """Result of RUFF format operation."""
33
+
34
+ formatted_code: str = Field(description="The formatted Python code")
35
+ changed: bool = Field(description="Whether the code was modified during formatting")
36
+
37
+
38
+ class VultureItem(BaseModel):
39
+ """Represents an unused code item found by VULTURE."""
40
+
41
+ name: str = Field(description="Name of the unused item")
42
+ type: str = Field(
43
+ description="Type of unused item (import, function, class, variable, etc.)"
44
+ )
45
+ line: int = Field(description="Line number where the unused item is defined")
46
+ column: int = Field(description="Column number where the unused item is defined")
47
+ confidence: int = Field(
48
+ description="Confidence level (0-100) that the item is unused"
49
+ )
50
+ message: str = Field(description="Description of the unused item")
51
+
52
+
53
+ class VultureScanResult(BaseModel):
54
+ """Result of VULTURE scan operation."""
55
+
56
+ unused_items: list[VultureItem] = Field(
57
+ description="List of unused code items found"
58
+ )
59
+ total_items: int = Field(description="Total number of unused items")
60
+ high_confidence_items: int = Field(
61
+ description="Number of items with confidence >= 80"
62
+ )
63
+
64
+
65
+ class AnalysisResult(BaseModel):
66
+ """Combined analysis result from both RUFF and VULTURE."""
67
+
68
+ ruff_result: RuffCheckResult = Field(description="RUFF linting results")
69
+ vulture_result: VultureScanResult = Field(
70
+ description="VULTURE dead code detection results"
71
+ )
72
+ summary: dict[str, int] = Field(description="Summary statistics of the analysis")
@@ -0,0 +1,264 @@
1
+ """MCP Python Analyzer Server - Main FastMCP server implementation."""
2
+
3
+ import logging
4
+ import sys
5
+ from typing import Any
6
+
7
+ from fastmcp import FastMCP
8
+
9
+ from mcp_python_analyzer.analyzers import RuffAnalyzer, VultureAnalyzer
10
+ from mcp_python_analyzer.models import (
11
+ AnalysisResult,
12
+ RuffCheckResult,
13
+ VultureScanResult,
14
+ )
15
+
16
+ # Initialize logging
17
+ logging.basicConfig(level=logging.INFO)
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Initialize FastMCP server
21
+ app: FastMCP[Any] = FastMCP("Python Analyzer")
22
+
23
+ # Initialize analyzers
24
+ ruff_analyzer = RuffAnalyzer()
25
+
26
+ # Try to initialize VULTURE analyzer, but handle gracefully if it fails
27
+ try:
28
+ vulture_analyzer: VultureAnalyzer | None = VultureAnalyzer()
29
+ vulture_available = True
30
+ except RuntimeError as e:
31
+ logger.warning("VULTURE not available: %s", e)
32
+ vulture_analyzer = None
33
+ vulture_available = False
34
+
35
+
36
+ @app.tool(name="ruff-check")
37
+ def ruff_check(code: str, config_path: str | None = None) -> dict[str, Any]:
38
+ """
39
+ Lint Python code using RUFF to identify style violations and potential errors.
40
+
41
+ Args:
42
+ code: Python code to analyze
43
+ config_path: Optional path to RUFF configuration file
44
+
45
+ Returns:
46
+ Dictionary containing linting results with issues, counts, and metadata
47
+ """
48
+ try:
49
+ result = ruff_analyzer.check_code(code, config_path)
50
+ return result.model_dump()
51
+ except Exception as e:
52
+ return {
53
+ "error": f"RUFF check failed: {e!s}",
54
+ "issues": [],
55
+ "total_issues": 0,
56
+ "fixable_issues": 0,
57
+ }
58
+
59
+
60
+ @app.tool(name="ruff-format")
61
+ def ruff_format(code: str, config_path: str | None = None) -> dict[str, Any]:
62
+ """
63
+ Format Python code using RUFF's fast formatter.
64
+
65
+ Args:
66
+ code: Python code to format
67
+ config_path: Optional path to RUFF configuration file
68
+
69
+ Returns:
70
+ Dictionary containing formatted code and change status
71
+ """
72
+ try:
73
+ result = ruff_analyzer.format_code(code, config_path)
74
+ return result.model_dump()
75
+ except Exception as e:
76
+ return {
77
+ "error": f"RUFF format failed: {e!s}",
78
+ "formatted_code": code, # Return original code on error
79
+ "changed": False,
80
+ }
81
+
82
+
83
+ @app.tool(name="ruff-check-ci")
84
+ def ruff_check_ci(
85
+ code: str, output_format: str = "json", config_path: str | None = None
86
+ ) -> dict[str, Any]:
87
+ """
88
+ Run RUFF linter with CI/CD-specific output formats.
89
+
90
+ Args:
91
+ code: Python code to lint
92
+ output_format: Output format (json, gitlab, github, sarif)
93
+ config_path: Optional path to RUFF configuration file
94
+
95
+ Returns:
96
+ Dictionary containing raw RUFF output in specified format
97
+ """
98
+ try:
99
+ result = ruff_analyzer.check_code_for_ci(code, output_format, config_path)
100
+ return {
101
+ "output": result,
102
+ "format": output_format,
103
+ "success": True,
104
+ }
105
+ except Exception as e:
106
+ return {
107
+ "error": f"RUFF CI check failed: {e!s}",
108
+ "output": "",
109
+ "format": output_format,
110
+ "success": False,
111
+ }
112
+
113
+
114
+ @app.tool(name="vulture-scan")
115
+ def vulture_scan(code: str, min_confidence: int = 80) -> dict[str, Any]:
116
+ """
117
+ Detect dead/unused code using VULTURE.
118
+
119
+ Args:
120
+ code: Python code to analyze
121
+ min_confidence: Minimum confidence level (0-100) for reporting items
122
+
123
+ Returns:
124
+ Dictionary containing unused code items with confidence scores and locations
125
+ """
126
+ if not vulture_available:
127
+ return {
128
+ "error": "VULTURE is not available - please install vulture package",
129
+ "unused_items": [],
130
+ "total_items": 0,
131
+ "high_confidence_items": 0,
132
+ }
133
+
134
+ try:
135
+ assert vulture_analyzer is not None # assure the type checker
136
+ result = vulture_analyzer.scan_code(code, min_confidence)
137
+ return result.model_dump()
138
+ except Exception as e:
139
+ return {
140
+ "error": f"VULTURE scan failed: {e!s}",
141
+ "unused_items": [],
142
+ "total_items": 0,
143
+ "high_confidence_items": 0,
144
+ }
145
+
146
+
147
+ @app.tool(name="analyze-code")
148
+ def analyze_code(
149
+ code: str,
150
+ ruff_config_path: str | None = None,
151
+ min_confidence: int = 80,
152
+ ) -> dict[str, Any]:
153
+ """
154
+ Comprehensive analysis combining RUFF linting and VULTURE dead code detection.
155
+
156
+ Args:
157
+ code: Python code to analyze
158
+ ruff_config_path: Optional path to RUFF configuration file
159
+ min_confidence: Minimum confidence level for VULTURE (default: 80)
160
+
161
+ Returns:
162
+ Dictionary containing combined analysis results with summary statistics
163
+ """
164
+ try:
165
+ # Run RUFF analysis
166
+ ruff_result = ruff_analyzer.check_code(code, ruff_config_path)
167
+
168
+ # Run VULTURE analysis if available
169
+ if vulture_available:
170
+ assert vulture_analyzer is not None # assure the type checker
171
+ vulture_result = vulture_analyzer.scan_code(code, min_confidence)
172
+ else:
173
+ # Create empty VULTURE result if not available
174
+ vulture_result = VultureScanResult(
175
+ unused_items=[],
176
+ total_items=0,
177
+ high_confidence_items=0,
178
+ )
179
+
180
+ # Create summary statistics
181
+ summary = {
182
+ "total_ruff_issues": ruff_result.total_issues,
183
+ "fixable_ruff_issues": ruff_result.fixable_issues,
184
+ "total_unused_items": vulture_result.total_items,
185
+ "high_confidence_unused": vulture_result.high_confidence_items,
186
+ "code_quality_score": _calculate_quality_score(ruff_result, vulture_result),
187
+ }
188
+
189
+ # Combine results
190
+ analysis = AnalysisResult(
191
+ ruff_result=ruff_result,
192
+ vulture_result=vulture_result,
193
+ summary=summary,
194
+ )
195
+
196
+ return analysis.model_dump()
197
+
198
+ except Exception as e:
199
+ return {
200
+ "error": f"Code analysis failed: {e!s}",
201
+ "ruff_result": {"issues": [], "total_issues": 0, "fixable_issues": 0},
202
+ "vulture_result": {
203
+ "unused_items": [],
204
+ "total_items": 0,
205
+ "high_confidence_items": 0,
206
+ },
207
+ "summary": {
208
+ "total_ruff_issues": 0,
209
+ "fixable_ruff_issues": 0,
210
+ "total_unused_items": 0,
211
+ "high_confidence_unused": 0,
212
+ "code_quality_score": 0,
213
+ },
214
+ }
215
+
216
+
217
+ def _calculate_quality_score(
218
+ ruff_result: RuffCheckResult, vulture_result: VultureScanResult
219
+ ) -> int:
220
+ """
221
+ Calculate a simple code quality score based on analysis results.
222
+
223
+ Args:
224
+ ruff_result: RUFF linting results
225
+ vulture_result: VULTURE scanning results
226
+
227
+ Returns:
228
+ Quality score from 0 to 100 (higher is better)
229
+ """
230
+ # Simple scoring algorithm - can be improved
231
+ base_score = 100
232
+
233
+ # Deduct points for RUFF issues
234
+ ruff_penalty = min(ruff_result.total_issues * 2, 50) # Max 50 points deduction
235
+
236
+ # Deduct points for high-confidence unused items
237
+ vulture_penalty = min(
238
+ vulture_result.high_confidence_items * 5, 30
239
+ ) # Max 30 points deduction
240
+
241
+ # Deduct points for total unused items (less severe)
242
+ total_unused_penalty = min(
243
+ (vulture_result.total_items - vulture_result.high_confidence_items) * 2, 20
244
+ ) # Max 20 points
245
+
246
+ # Replace temp variable with direct return
247
+ return max(0, base_score - ruff_penalty - vulture_penalty - total_unused_penalty)
248
+
249
+
250
+ def main() -> None:
251
+ """Main entry point for the MCP server."""
252
+ try:
253
+ # Run the FastMCP server
254
+ app.run()
255
+ except KeyboardInterrupt:
256
+ logger.info("Server stopped by user")
257
+ sys.exit(0)
258
+ except Exception as e:
259
+ logger.error("Server error: %s", e)
260
+ sys.exit(1)
261
+
262
+
263
+ if __name__ == "__main__":
264
+ main()
@@ -0,0 +1,265 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-server-analyzer
3
+ Version: 0.1.0
4
+ Summary: MCP server for Python code analysis with RUFF linting and VULTURE dead code detection
5
+ Project-URL: Homepage, https://github.com/anselmoo/mcp-server-analyzer
6
+ Project-URL: Documentation, https://github.com/anselmoo/mcp-server-analyzer
7
+ Project-URL: Repository, https://github.com/anselmoo/mcp-server-analyzer
8
+ Project-URL: Issues, https://github.com/anselmoo/mcp-server-analyzer/issues
9
+ Author-email: Anselm Hahn <anselm.hahn@gmail.com>
10
+ Maintainer-email: Anselm Hahn <anselm.hahn@gmail.com>
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: code-analysis,linting,mcp,python,ruff,static-analysis,vulture
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: fastmcp>=0.3.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Requires-Dist: ruff>=0.8.0
27
+ Requires-Dist: vulture>=2.11
28
+ Provides-Extra: dev
29
+ Requires-Dist: bandit>=1.7.0; extra == 'dev'
30
+ Requires-Dist: black>=23.0.0; extra == 'dev'
31
+ Requires-Dist: mypy>=1.0.0; extra == 'dev'
32
+ Requires-Dist: pre-commit>=3.0.0; extra == 'dev'
33
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
34
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
35
+ Requires-Dist: pytest>=8.4.1; extra == 'dev'
36
+ Requires-Dist: safety>=2.0.0; extra == 'dev'
37
+ Provides-Extra: test
38
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'test'
39
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'test'
40
+ Requires-Dist: pytest>=8.4.1; extra == 'test'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # MCP Python Analyzer Server ๐Ÿ๐Ÿ”
44
+
45
+ [![CI/CD Pipeline](https://github.com/anselmoo/mcp-server-analyzer/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/anselmoo/mcp-server-analyzer/actions/workflows/ci-cd.yml)
46
+ [![PyPI version](https://badge.fury.io/py/mcp-server-analyzer.svg)](https://badge.fury.io/py/mcp-server-analyzer)
47
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
48
+ [![Docker](https://img.shields.io/badge/docker-available-blue.svg)](https://github.com/anselmoo/mcp-server-analyzer/pkgs/container/mcp-server-analyzer)
49
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
50
+ [![Code Coverage](https://codecov.io/gh/anselmoo/mcp-server-analyzer/branch/main/graph/badge.svg)](https://codecov.io/gh/anselmoo/mcp-server-analyzer)
51
+
52
+ A powerful [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that provides comprehensive Python code analysis using [**RUFF**](https://docs.astral.sh/ruff/) for linting and [**VULTURE**](https://github.com/jendrikseipp/vulture) for dead code detection. Perfect for AI assistants, IDEs, and automated code review workflows.
53
+
54
+ ## ๐Ÿš€ Quick Start
55
+
56
+ ### VS Code Integration (One-Click Install)
57
+
58
+ For quick installation, use one of the one-click install buttons below...
59
+
60
+ [![Install with UV in VS Code](https://img.shields.io/badge/VS_Code-UV-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=analyzer&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-analyzer%22%5D%7D) [![Install with UV in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-UV-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=analyzer&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-analyzer%22%5D%7D&quality=insiders)
61
+
62
+ [![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Docker-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=analyzer&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22ghcr.io%2Fanselmoo%2Fmcp-server-analyzer%22%5D%7D) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Docker-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=analyzer&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22ghcr.io%2Fanselmoo%2Fmcp-server-analyzer%22%5D%7D&quality=insiders)
63
+
64
+ For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
65
+
66
+ Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
67
+
68
+ > Note that the `mcp` key is needed when using the `mcp.json` file.
69
+
70
+ **Using uvx (recommended):**
71
+
72
+ ```json
73
+ {
74
+ "mcp": {
75
+ "servers": {
76
+ "analyzer": {
77
+ "command": "uvx",
78
+ "args": ["mcp-server-analyzer"]
79
+ }
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ **Using Docker:**
86
+
87
+ ```json
88
+ {
89
+ "mcp": {
90
+ "servers": {
91
+ "analyzer": {
92
+ "command": "docker",
93
+ "args": ["run", "-i", "--rm", "ghcr.io/anselmoo/mcp-server-analyzer"]
94
+ }
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ ### Universal Installation
101
+
102
+ ```bash
103
+ # Install with uvx (recommended)
104
+ uvx install mcp-server-analyzer
105
+
106
+ # Install with pip
107
+ pip install mcp-server-analyzer
108
+
109
+ # Run with Docker
110
+ docker run ghcr.io/anselmoo/mcp-server-analyzer:latest
111
+
112
+ # Install from source
113
+ git clone https://github.com/anselmoo/mcp-server-analyzer.git
114
+ cd mcp-server-analyzer
115
+ uv sync --dev
116
+ uv run mcp-server-analyzer
117
+ ```
118
+
119
+ ## ๐Ÿ“‹ Features
120
+
121
+ - **๐Ÿ” RUFF Analysis**: Comprehensive Python linting with auto-fixes
122
+ - **๐Ÿงน Dead Code Detection**: Find unused imports, functions, and variables with VULTURE
123
+ - **๐Ÿ“Š Quality Scoring**: Combined analysis with quality metrics
124
+ - **๐Ÿš€ FastMCP Framework**: High-performance MCP server implementation
125
+ - **๐Ÿณ Docker Ready**: Multi-architecture containers with security signing
126
+ - **๐Ÿ”’ Secure**: All releases signed with Sigstore for supply chain security
127
+
128
+ ## ๐Ÿ“ˆ Analysis Examples
129
+
130
+ ### RUFF Linting Preview
131
+
132
+ See comprehensive linting analysis examples: **[๐Ÿ“‹ RUFF Analysis Preview](examples/preview-ruff.md)**
133
+
134
+ ### VULTURE Dead Code Detection Preview
135
+
136
+ Explore dead code detection capabilities: **[๐Ÿงน VULTURE Analysis Preview](examples/preview-vulture.md)**
137
+
138
+ ## ๐Ÿ› ๏ธ Available Tools
139
+
140
+ | Tool | Description | Use Case |
141
+ | --------------- | -------------------------------- | ------------------------------------ |
142
+ | `ruff-check` | Lint Python code with RUFF | Style violations, potential errors |
143
+ | `ruff-format` | Format Python code with RUFF | Code formatting and consistency |
144
+ | `ruff-check-ci` | CI/CD optimized RUFF output | GitHub Actions, GitLab CI |
145
+ | `vulture-scan` | Dead code detection | Unused imports, functions, variables |
146
+ | `analyze-code` | Combined RUFF + VULTURE analysis | Complete code quality assessment |
147
+
148
+ ## ๐Ÿ”ง Configuration
149
+
150
+ ### Claude Desktop
151
+
152
+ Add to your `claude_desktop_config.json`:
153
+
154
+ ```json
155
+ {
156
+ "mcpServers": {
157
+ "analyzer": {
158
+ "command": "uvx",
159
+ "args": ["mcp-server-analyzer"]
160
+ }
161
+ }
162
+ }
163
+ ```
164
+
165
+ ### Zed
166
+
167
+ Add to your Zed settings.json:
168
+
169
+ ```json
170
+ "context_servers": {
171
+ "analyzer": {
172
+ "command": "uvx",
173
+ "args": ["mcp-server-analyzer"]
174
+ }
175
+ }
176
+ ```
177
+
178
+ ## ๐Ÿงช Development
179
+
180
+ ### Prerequisites
181
+
182
+ - Python 3.10+
183
+ - [uv](https://docs.astral.sh/uv/) (recommended) or pip
184
+ - [Docker](https://docker.com) (optional)
185
+
186
+ ### Setup
187
+
188
+ ```bash
189
+ # Clone repository
190
+ git clone https://github.com/anselmoo/mcp-server-analyzer.git
191
+ cd mcp-server-analyzer
192
+
193
+ # Install dependencies
194
+ uv sync --dev
195
+
196
+ # Run tests
197
+ uv run pytest
198
+
199
+ # Run pre-commit hooks
200
+ uv tool run pre-commit run --all-files
201
+
202
+ # Build Docker image
203
+ docker build -t mcp-server-analyzer .
204
+ ```
205
+
206
+ ### Testing
207
+
208
+ ```bash
209
+ # Run all tests
210
+ uv run pytest tests/ -v
211
+
212
+ # Run with coverage
213
+ uv run pytest --cov=src/mcp_python_analyzer --cov-report=html
214
+
215
+ # Test specific functionality
216
+ uv run pytest tests/test_server.py::TestAnalyzer::test_ruff_analysis
217
+ ```
218
+
219
+ ## ๐Ÿ“Š Quality Metrics
220
+
221
+ The server provides quality scoring based on:
222
+
223
+ - **RUFF Issues**: Style violations, potential bugs, complexity metrics
224
+ - **Dead Code Detection**: Unused imports, functions, variables
225
+ - **Combined Score**: Weighted quality assessment (0-100)
226
+
227
+ ## ๐Ÿ”’ Security
228
+
229
+ - **Signed Releases**: All releases signed with [Sigstore](https://sigstore.dev/)
230
+ - **Container Signing**: Docker images signed with [Cosign](https://docs.sigstore.dev/cosign/overview/)
231
+ - **Trusted Publishing**: PyPI releases use GitHub OIDC trusted publishing
232
+ - **Vulnerability Scanning**: Automated security scanning in CI/CD
233
+ - **Supply Chain Security**: SLSA Build Level 3 compliance
234
+
235
+ ## ๐Ÿ“š Documentation
236
+
237
+ - **[MCP Specification](https://modelcontextprotocol.io/)** - Learn about Model Context Protocol
238
+ - **[FastMCP Framework](https://gofastmcp.com/)** - High-performance MCP implementation
239
+ - **[RUFF Documentation](https://docs.astral.sh/ruff/)** - Python linter and formatter
240
+ - **[VULTURE Documentation](https://github.com/jendrikseipp/vulture)** - Dead code finder
241
+
242
+ ## ๐Ÿค Contributing
243
+
244
+ Contributions are welcome! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
245
+
246
+ 1. Fork the repository
247
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
248
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
249
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
250
+ 5. Open a Pull Request
251
+
252
+ ## ๐Ÿ“ License
253
+
254
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
255
+
256
+ ## ๐Ÿ™ Acknowledgments
257
+
258
+ - [Astral](https://astral.sh/) for RUFF and uv
259
+ - [Jendrik Seipp](https://github.com/jendrikseipp) for VULTURE
260
+ - [Model Context Protocol](https://modelcontextprotocol.io/) team
261
+ - [FastMCP](https://gofastmcp.com/) framework
262
+
263
+ ---
264
+
265
+ **Made with โค๏ธ for better Python code quality**
@@ -0,0 +1,12 @@
1
+ mcp_python_analyzer/__init__.py,sha256=bPIYonewbfJGGIqYmUtLQjkHEbt24GX4u7Ds7MnyvgI,237
2
+ mcp_python_analyzer/__main__.py,sha256=HOKceDWn0JvyXdMZHonU1V9sTkNQP6a9Oqz_1RPmTVc,166
3
+ mcp_python_analyzer/models.py,sha256=honpIBYerxcoC11GTEXwuMnmU_Wbihp_AgvFbUGXfCo,2748
4
+ mcp_python_analyzer/server.py,sha256=9QCuTp-UCH4zP4cehP8L8jP4IZKjTGdqPHHIMeuQj9s,7925
5
+ mcp_python_analyzer/analyzers/__init__.py,sha256=_TP4IrLzNrRcoEOexr5_AkWu6L_WyLSjRrIzPma5l9E,174
6
+ mcp_python_analyzer/analyzers/ruff.py,sha256=iiHb4LSUc2fc-4BVpe1etI27w1neaJ4oOnKre4Cw-dY,6495
7
+ mcp_python_analyzer/analyzers/vulture.py,sha256=CpQV4D5m4Zy3UwzmSJu9h1OzJUCS3oYAU5Cw2jLFKFo,6672
8
+ mcp_server_analyzer-0.1.0.dist-info/METADATA,sha256=_AVDTybW5B8TsbTRR3j93y_b8GMQsROHFbRbGgb2r0I,10159
9
+ mcp_server_analyzer-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ mcp_server_analyzer-0.1.0.dist-info/entry_points.txt,sha256=SpeNZYsdCOS8w8AJo9bXhK-i9n9DBv_dgkxtycVlN_E,72
11
+ mcp_server_analyzer-0.1.0.dist-info/licenses/LICENSE,sha256=FqeVsqKpPjbVPTy3xbdFxzRvWd8GPgI0Hx3i_oAWcJQ,1068
12
+ mcp_server_analyzer-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcp-server-analyzer = mcp_python_analyzer.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Anselm Hahn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.