maven-decoder-mcp 1.0.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,35 @@
1
+ """
2
+ Maven Decoder MCP Server
3
+
4
+ An MCP (Model Context Protocol) server for reading and decompiling Maven .m2 jar files.
5
+ Provides comprehensive Java project analysis including:
6
+ - Jar file exploration and analysis
7
+ - Java class decompilation (CFR, Procyon, javap)
8
+ - Maven dependency resolution
9
+ - Source code extraction
10
+ - Class structure analysis
11
+
12
+ Usage:
13
+ from maven_decoder_mcp import MavenDecoderServer
14
+
15
+ # Create and run server
16
+ server = MavenDecoderServer()
17
+ server.run()
18
+
19
+ Command line:
20
+ maven-decoder-mcp
21
+ """
22
+
23
+ __version__ = "1.0.0"
24
+ __author__ = "Your Name"
25
+ __email__ = "your-email@example.com"
26
+
27
+ from .maven_decoder_server import MavenDecoderServer
28
+ from .decompiler import JavaDecompiler
29
+ from .maven_analyzer import MavenDependencyAnalyzer
30
+
31
+ __all__ = [
32
+ "MavenDecoderServer",
33
+ "JavaDecompiler",
34
+ "MavenDependencyAnalyzer",
35
+ ]
@@ -0,0 +1,108 @@
1
+ """
2
+ Configuration module for Maven Decoder MCP Server
3
+ """
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ class Config:
10
+ """Configuration settings for the Maven Decoder MCP Server"""
11
+
12
+ # Maven repository location
13
+ MAVEN_HOME: Path = Path.home() / ".m2" / "repository"
14
+
15
+ # Override Maven home from environment variable
16
+ if "M2_HOME" in os.environ:
17
+ MAVEN_HOME = Path(os.environ["M2_HOME"]) / "repository"
18
+ elif "MAVEN_REPO" in os.environ:
19
+ MAVEN_HOME = Path(os.environ["MAVEN_REPO"])
20
+
21
+ # Server configuration
22
+ SERVER_NAME: str = "maven-decoder"
23
+ SERVER_VERSION: str = "1.0.0"
24
+
25
+ # Decompiler settings
26
+ DECOMPILER_TIMEOUT: int = 30 # seconds
27
+ DECOMPILER_PRIORITY: list = ["cfr", "procyon", "fernflower", "javap"]
28
+
29
+ # Search and analysis limits
30
+ DEFAULT_ARTIFACT_LIMIT: int = 50
31
+ DEFAULT_SEARCH_LIMIT: int = 100
32
+ MAX_DEPENDENCY_DEPTH: int = 5
33
+
34
+ # Cache settings
35
+ ENABLE_CACHE: bool = True
36
+ CACHE_SIZE: int = 1000
37
+ CACHE_TTL: int = 3600 # seconds
38
+
39
+ # Logging configuration
40
+ LOG_LEVEL: str = os.environ.get("LOG_LEVEL", "INFO")
41
+ LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
42
+
43
+ # Performance settings
44
+ MAX_CONCURRENT_OPERATIONS: int = 10
45
+ JAR_ANALYSIS_TIMEOUT: int = 60 # seconds
46
+
47
+ # Feature flags
48
+ ENABLE_DECOMPILATION: bool = True
49
+ ENABLE_TRANSITIVE_DEPS: bool = True
50
+ ENABLE_BYTECODE_ANALYSIS: bool = False # Requires additional libraries
51
+
52
+ # File size limits (in bytes)
53
+ MAX_JAR_SIZE: int = 100 * 1024 * 1024 # 100MB
54
+ MAX_CLASS_SIZE: int = 1024 * 1024 # 1MB
55
+
56
+ @classmethod
57
+ def validate(cls) -> bool:
58
+ """Validate configuration settings"""
59
+ if not cls.MAVEN_HOME.exists():
60
+ print(f"Warning: Maven repository not found at {cls.MAVEN_HOME}")
61
+ return False
62
+
63
+ if not cls.MAVEN_HOME.is_dir():
64
+ print(f"Error: Maven repository path is not a directory: {cls.MAVEN_HOME}")
65
+ return False
66
+
67
+ return True
68
+
69
+ @classmethod
70
+ def get_maven_home(cls) -> Path:
71
+ """Get the Maven repository home directory"""
72
+ return cls.MAVEN_HOME
73
+
74
+ @classmethod
75
+ def set_maven_home(cls, path: str) -> None:
76
+ """Set a custom Maven repository location"""
77
+ cls.MAVEN_HOME = Path(path)
78
+
79
+ @classmethod
80
+ def get_decompiler_config(cls) -> dict:
81
+ """Get decompiler configuration"""
82
+ return {
83
+ "timeout": cls.DECOMPILER_TIMEOUT,
84
+ "priority": cls.DECOMPILER_PRIORITY,
85
+ "enabled": cls.ENABLE_DECOMPILATION
86
+ }
87
+
88
+ @classmethod
89
+ def get_limits(cls) -> dict:
90
+ """Get search and analysis limits"""
91
+ return {
92
+ "artifacts": cls.DEFAULT_ARTIFACT_LIMIT,
93
+ "search": cls.DEFAULT_SEARCH_LIMIT,
94
+ "dependency_depth": cls.MAX_DEPENDENCY_DEPTH,
95
+ "max_jar_size": cls.MAX_JAR_SIZE,
96
+ "max_class_size": cls.MAX_CLASS_SIZE
97
+ }
98
+
99
+ # Environment-specific configuration overrides
100
+ if os.environ.get("MAVEN_DECODER_ENV") == "development":
101
+ Config.LOG_LEVEL = "DEBUG"
102
+ Config.ENABLE_BYTECODE_ANALYSIS = True
103
+ Config.CACHE_TTL = 60 # Shorter cache in development
104
+
105
+ elif os.environ.get("MAVEN_DECODER_ENV") == "production":
106
+ Config.LOG_LEVEL = "WARNING"
107
+ Config.MAX_CONCURRENT_OPERATIONS = 20
108
+ Config.CACHE_SIZE = 5000
@@ -0,0 +1,329 @@
1
+ """
2
+ Java Decompiler Integration for Maven Decoder MCP Server
3
+
4
+ Provides integration with various Java decompilers and bytecode analysis tools.
5
+ """
6
+
7
+ import subprocess
8
+ import tempfile
9
+ import os
10
+ import zipfile
11
+ from pathlib import Path
12
+ from typing import Optional, Dict, Any, List
13
+ import logging
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class JavaDecompiler:
18
+ """Java bytecode decompiler and analyzer"""
19
+
20
+ def __init__(self):
21
+ self.available_decompilers = self._detect_decompilers()
22
+
23
+ def _detect_decompilers(self) -> Dict[str, str]:
24
+ """Detect available decompilers on the system"""
25
+ decompilers = {}
26
+
27
+ # Check for CFR (free Java decompiler)
28
+ try:
29
+ cfr_paths = ['cfr.jar', 'decompilers/cfr.jar']
30
+ for cfr_path in cfr_paths:
31
+ if Path(cfr_path).exists():
32
+ result = subprocess.run(['java', '-jar', cfr_path, '--help'],
33
+ capture_output=True, text=True, timeout=5)
34
+ if result.returncode == 0:
35
+ decompilers['cfr'] = cfr_path
36
+ break
37
+ except Exception:
38
+ pass
39
+
40
+ # Check for Fernflower (IntelliJ's decompiler)
41
+ try:
42
+ if Path('fernflower.jar').exists():
43
+ result = subprocess.run(['java', '-jar', 'fernflower.jar'],
44
+ capture_output=True, text=True, timeout=5)
45
+ decompilers['fernflower'] = 'fernflower.jar'
46
+ except Exception:
47
+ pass
48
+
49
+ # Check for Procyon
50
+ try:
51
+ procyon_paths = ['procyon-decompiler.jar', 'decompilers/procyon-decompiler.jar']
52
+ for procyon_path in procyon_paths:
53
+ if Path(procyon_path).exists():
54
+ result = subprocess.run(['java', '-jar', procyon_path, '--help'],
55
+ capture_output=True, text=True, timeout=5)
56
+ if result.returncode == 0:
57
+ decompilers['procyon'] = procyon_path
58
+ break
59
+ except Exception:
60
+ pass
61
+
62
+ # Check for javap (built-in with JDK)
63
+ try:
64
+ result = subprocess.run(['javap', '-help'],
65
+ capture_output=True, text=True, timeout=5)
66
+ if result.returncode == 0:
67
+ decompilers['javap'] = 'javap'
68
+ except Exception:
69
+ pass
70
+
71
+ return decompilers
72
+
73
+ def decompile_class(self, jar_path: Path, class_name: str,
74
+ decompiler: Optional[str] = None) -> Optional[str]:
75
+ """Decompile a specific class from a jar file"""
76
+ logger.info(f"Attempting to decompile {class_name} from {jar_path}")
77
+
78
+ if not self.available_decompilers:
79
+ logger.warning("No decompilers available, using fallback")
80
+ return self._fallback_class_info(jar_path, class_name)
81
+
82
+ # Choose decompiler
83
+ if not decompiler:
84
+ decompiler = next(iter(self.available_decompilers.keys()))
85
+
86
+ if decompiler not in self.available_decompilers:
87
+ logger.warning(f"Decompiler {decompiler} not available")
88
+ return self._fallback_class_info(jar_path, class_name)
89
+
90
+ logger.info(f"Using decompiler: {decompiler}")
91
+
92
+ try:
93
+ with tempfile.TemporaryDirectory() as temp_dir:
94
+ temp_path = Path(temp_dir)
95
+
96
+ # Extract class file
97
+ class_file_path = class_name.replace('.', '/') + '.class'
98
+ logger.debug(f"Looking for class file: {class_file_path}")
99
+
100
+ with zipfile.ZipFile(jar_path, 'r') as jar:
101
+ if class_file_path not in jar.namelist():
102
+ logger.error(f"Class file not found in jar: {class_file_path}")
103
+ return self._fallback_class_info(jar_path, class_name)
104
+
105
+ # Extract the class file
106
+ extracted_class = temp_path / 'extracted.class'
107
+ logger.debug(f"Extracting to: {extracted_class}")
108
+ with open(extracted_class, 'wb') as f:
109
+ f.write(jar.read(class_file_path))
110
+
111
+ # Decompile
112
+ result = self._run_decompiler(decompiler, extracted_class, temp_path)
113
+ if result:
114
+ logger.info(f"Decompilation successful with {decompiler}")
115
+ return result
116
+ else:
117
+ logger.warning(f"Decompilation failed with {decompiler}, trying fallback")
118
+ return self._fallback_class_info(jar_path, class_name)
119
+
120
+ except Exception as e:
121
+ logger.error(f"Decompilation failed: {e}")
122
+ return self._fallback_class_info(jar_path, class_name)
123
+
124
+ def _run_decompiler(self, decompiler: str, class_file: Path, temp_dir: Path) -> Optional[str]:
125
+ """Run the specified decompiler"""
126
+ logger.debug(f"Running decompiler {decompiler} on {class_file}")
127
+
128
+ try:
129
+ if decompiler == 'javap':
130
+ # Use javap for disassembly
131
+ logger.debug("Using javap for disassembly")
132
+ result = subprocess.run([
133
+ 'javap', '-v', '-p', '-c', str(class_file)
134
+ ], capture_output=True, text=True, timeout=30)
135
+
136
+ logger.debug(f"javap return code: {result.returncode}")
137
+ if result.stderr:
138
+ logger.warning(f"javap stderr: {result.stderr}")
139
+
140
+ if result.returncode == 0:
141
+ return result.stdout
142
+ else:
143
+ logger.error(f"javap failed with return code {result.returncode}: {result.stderr}")
144
+
145
+ elif decompiler == 'cfr':
146
+ # Use CFR decompiler
147
+ output_dir = temp_dir / 'output'
148
+ output_dir.mkdir()
149
+
150
+ result = subprocess.run([
151
+ 'java', '-jar', self.available_decompilers[decompiler],
152
+ str(class_file), '--outputdir', str(output_dir)
153
+ ], capture_output=True, text=True, timeout=30)
154
+
155
+ # Look for generated .java file
156
+ java_files = list(output_dir.rglob('*.java'))
157
+ if java_files:
158
+ with open(java_files[0], 'r', encoding='utf-8') as f:
159
+ return f.read()
160
+
161
+ elif decompiler == 'procyon':
162
+ # Use Procyon decompiler
163
+ result = subprocess.run([
164
+ 'java', '-jar', self.available_decompilers[decompiler],
165
+ str(class_file)
166
+ ], capture_output=True, text=True, timeout=30)
167
+
168
+ if result.returncode == 0:
169
+ return result.stdout
170
+
171
+ elif decompiler == 'fernflower':
172
+ # Use Fernflower decompiler
173
+ output_dir = temp_dir / 'output'
174
+ output_dir.mkdir()
175
+
176
+ result = subprocess.run([
177
+ 'java', '-jar', self.available_decompilers[decompiler],
178
+ str(class_file), str(output_dir)
179
+ ], capture_output=True, text=True, timeout=30)
180
+
181
+ # Look for generated .java file
182
+ java_files = list(output_dir.rglob('*.java'))
183
+ if java_files:
184
+ with open(java_files[0], 'r', encoding='utf-8') as f:
185
+ return f.read()
186
+
187
+ except Exception as e:
188
+ logger.error(f"Decompiler {decompiler} failed: {e}")
189
+
190
+ return None
191
+
192
+ def _fallback_class_info(self, jar_path: Path, class_name: str) -> str:
193
+ """Fallback to basic class information when decompilation fails"""
194
+ try:
195
+ class_file_path = class_name.replace('.', '/') + '.class'
196
+
197
+ with zipfile.ZipFile(jar_path, 'r') as jar:
198
+ if class_file_path in jar.namelist():
199
+ class_data = jar.read(class_file_path)
200
+
201
+ # Basic bytecode analysis
202
+ info = f"// Decompilation not available\n"
203
+ info += f"// Class: {class_name}\n"
204
+ info += f"// Size: {len(class_data)} bytes\n"
205
+ info += f"// Location: {jar_path}\n\n"
206
+
207
+ # Try to extract some basic info from bytecode
208
+ magic = class_data[:4]
209
+ if magic == b'\xca\xfe\xba\xbe':
210
+ minor_version = int.from_bytes(class_data[4:6], 'big')
211
+ major_version = int.from_bytes(class_data[6:8], 'big')
212
+ info += f"// Java bytecode version: {major_version}.{minor_version}\n"
213
+
214
+ # Map major version to Java version
215
+ java_version = self._map_bytecode_version(major_version)
216
+ if java_version:
217
+ info += f"// Compiled for Java: {java_version}\n"
218
+
219
+ info += f"\npublic class {class_name.split('.')[-1]} {{\n"
220
+ info += " // Decompilation requires external tools\n"
221
+ info += " // Install CFR, Procyon, or Fernflower for full decompilation\n"
222
+ info += "}\n"
223
+
224
+ return info
225
+
226
+ except Exception as e:
227
+ return f"// Error reading class file: {e}"
228
+
229
+ return f"// Class not found: {class_name}"
230
+
231
+ def _map_bytecode_version(self, major_version: int) -> Optional[str]:
232
+ """Map bytecode major version to Java version"""
233
+ version_map = {
234
+ 45: "1.1",
235
+ 46: "1.2",
236
+ 47: "1.3",
237
+ 48: "1.4",
238
+ 49: "5",
239
+ 50: "6",
240
+ 51: "7",
241
+ 52: "8",
242
+ 53: "9",
243
+ 54: "10",
244
+ 55: "11",
245
+ 56: "12",
246
+ 57: "13",
247
+ 58: "14",
248
+ 59: "15",
249
+ 60: "16",
250
+ 61: "17",
251
+ 62: "18",
252
+ 63: "19",
253
+ 64: "20",
254
+ 65: "21"
255
+ }
256
+ return version_map.get(major_version)
257
+
258
+ def analyze_jar_structure(self, jar_path: Path) -> Dict[str, Any]:
259
+ """Analyze the overall structure of a jar file"""
260
+ analysis = {
261
+ "jar_path": str(jar_path),
262
+ "total_size": jar_path.stat().st_size,
263
+ "packages": {},
264
+ "class_count": 0,
265
+ "resource_count": 0,
266
+ "manifest": {},
267
+ "services": [],
268
+ "annotations": []
269
+ }
270
+
271
+ try:
272
+ with zipfile.ZipFile(jar_path, 'r') as jar:
273
+ entries = jar.namelist()
274
+
275
+ # Analyze entries
276
+ for entry in entries:
277
+ if entry.endswith('.class'):
278
+ analysis["class_count"] += 1
279
+
280
+ # Extract package info
281
+ if '/' in entry:
282
+ package = '/'.join(entry.split('/')[:-1]).replace('/', '.')
283
+ if package not in analysis["packages"]:
284
+ analysis["packages"][package] = 0
285
+ analysis["packages"][package] += 1
286
+
287
+ elif not entry.endswith('/'):
288
+ analysis["resource_count"] += 1
289
+
290
+ # Read manifest
291
+ if "META-INF/MANIFEST.MF" in entries:
292
+ manifest_content = jar.read("META-INF/MANIFEST.MF").decode('utf-8', errors='ignore')
293
+ analysis["manifest"] = self._parse_manifest(manifest_content)
294
+
295
+ # Look for services
296
+ service_entries = [e for e in entries if e.startswith("META-INF/services/")]
297
+ for service_entry in service_entries:
298
+ service_name = service_entry.replace("META-INF/services/", "")
299
+ try:
300
+ service_content = jar.read(service_entry).decode('utf-8', errors='ignore')
301
+ analysis["services"].append({
302
+ "interface": service_name,
303
+ "implementations": [line.strip() for line in service_content.split('\n') if line.strip()]
304
+ })
305
+ except Exception:
306
+ pass
307
+
308
+ except Exception as e:
309
+ analysis["error"] = str(e)
310
+
311
+ return analysis
312
+
313
+ def _parse_manifest(self, manifest_content: str) -> Dict[str, str]:
314
+ """Parse JAR manifest file"""
315
+ manifest = {}
316
+ current_key = None
317
+
318
+ for line in manifest_content.split('\n'):
319
+ line = line.rstrip('\r')
320
+
321
+ if line.startswith(' ') and current_key:
322
+ # Continuation line
323
+ manifest[current_key] += line[1:]
324
+ elif ':' in line:
325
+ key, value = line.split(':', 1)
326
+ current_key = key.strip()
327
+ manifest[current_key] = value.strip()
328
+
329
+ return manifest
@@ -0,0 +1,11 @@
1
+ """
2
+ Decompiler paths configuration
3
+ Auto-generated by setup_decompilers.sh
4
+ """
5
+
6
+ DECOMPILER_PATHS = {
7
+ 'cfr': 'decompilers/cfr.jar',
8
+ 'procyon': 'decompilers/procyon-decompiler.jar',
9
+ # Uncomment and update path if you have Fernflower
10
+ # 'fernflower': 'path/to/java-decompiler.jar'
11
+ }