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.
- mcp_python_analyzer/__init__.py +7 -0
- mcp_python_analyzer/__main__.py +7 -0
- mcp_python_analyzer/analyzers/__init__.py +6 -0
- mcp_python_analyzer/analyzers/ruff.py +188 -0
- mcp_python_analyzer/analyzers/vulture.py +194 -0
- mcp_python_analyzer/models.py +72 -0
- mcp_python_analyzer/server.py +264 -0
- mcp_server_analyzer-0.1.0.dist-info/METADATA +265 -0
- mcp_server_analyzer-0.1.0.dist-info/RECORD +12 -0
- mcp_server_analyzer-0.1.0.dist-info/WHEEL +4 -0
- mcp_server_analyzer-0.1.0.dist-info/entry_points.txt +2 -0
- mcp_server_analyzer-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|
+
[](https://github.com/anselmoo/mcp-server-analyzer/actions/workflows/ci-cd.yml)
|
|
46
|
+
[](https://badge.fury.io/py/mcp-server-analyzer)
|
|
47
|
+
[](https://www.python.org/downloads/)
|
|
48
|
+
[](https://github.com/anselmoo/mcp-server-analyzer/pkgs/container/mcp-server-analyzer)
|
|
49
|
+
[](https://opensource.org/licenses/MIT)
|
|
50
|
+
[](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
|
+
[](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) [](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
|
+
[](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) [](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,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.
|