java-class-analyzer-mcp 0.1.1__tar.gz → 0.1.2__tar.gz
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.
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/PKG-INFO +1 -19
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/README.md +0 -18
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp/analyzer/java_class_analyzer.py +10 -10
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp/decompiler/decompiler_service.py +28 -25
- java_class_analyzer_mcp-0.1.2/java_class_analyzer_mcp/logger.py +99 -0
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp/main.py +19 -1
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp/scanner/dependency_scanner.py +21 -26
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp.egg-info/PKG-INFO +1 -19
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp.egg-info/SOURCES.txt +1 -0
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/pyproject.toml +1 -1
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp/__init__.py +0 -0
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp/analyzer/__init__.py +0 -0
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp/cfr-0.152.jar +0 -0
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp/cli/__init__.py +0 -0
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp/cli.py +0 -0
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp/decompiler/__init__.py +0 -0
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp/scanner/__init__.py +0 -0
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp.egg-info/dependency_links.txt +0 -0
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp.egg-info/entry_points.txt +0 -0
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp.egg-info/requires.txt +0 -0
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp.egg-info/top_level.txt +0 -0
- {java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: java-class-analyzer-mcp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: MCP server for scanning Maven dependencies and decompiling Java classes
|
|
5
5
|
Author: java-class-analyzer-mcp contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -150,24 +150,6 @@ java-class-analyzer-mcp/
|
|
|
150
150
|
- `className` (string): 要分析的类全名
|
|
151
151
|
- `projectPath` (string): Maven 项目根目录路径
|
|
152
152
|
|
|
153
|
-
## 命令行工具
|
|
154
|
-
|
|
155
|
-
安装后可直接使用命令行工具:
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
# 启动 MCP 服务器(stdio 模式,供 MCP 客户端调用)
|
|
159
|
-
java-class-analyzer-mcp
|
|
160
|
-
|
|
161
|
-
# 生成 MCP 配置模板
|
|
162
|
-
java-class-analyzer-mcp config -o mcp-config.json
|
|
163
|
-
|
|
164
|
-
# 测试工具
|
|
165
|
-
java-class-analyzer-mcp test -t scan -p /path/to/project
|
|
166
|
-
java-class-analyzer-mcp test -t decompile -p /path/to/project -c com.example.MyClass
|
|
167
|
-
java-class-analyzer-mcp test -t analyze -p /path/to/project -c com.example.MyClass
|
|
168
|
-
java-class-analyzer-mcp test -t all -p /path/to/project -c com.example.MyClass --no-cache
|
|
169
|
-
```
|
|
170
|
-
|
|
171
153
|
## 缓存文件
|
|
172
154
|
|
|
173
155
|
在项目目录下会生成以下缓存:
|
|
@@ -135,24 +135,6 @@ java-class-analyzer-mcp/
|
|
|
135
135
|
- `className` (string): 要分析的类全名
|
|
136
136
|
- `projectPath` (string): Maven 项目根目录路径
|
|
137
137
|
|
|
138
|
-
## 命令行工具
|
|
139
|
-
|
|
140
|
-
安装后可直接使用命令行工具:
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
# 启动 MCP 服务器(stdio 模式,供 MCP 客户端调用)
|
|
144
|
-
java-class-analyzer-mcp
|
|
145
|
-
|
|
146
|
-
# 生成 MCP 配置模板
|
|
147
|
-
java-class-analyzer-mcp config -o mcp-config.json
|
|
148
|
-
|
|
149
|
-
# 测试工具
|
|
150
|
-
java-class-analyzer-mcp test -t scan -p /path/to/project
|
|
151
|
-
java-class-analyzer-mcp test -t decompile -p /path/to/project -c com.example.MyClass
|
|
152
|
-
java-class-analyzer-mcp test -t analyze -p /path/to/project -c com.example.MyClass
|
|
153
|
-
java-class-analyzer-mcp test -t all -p /path/to/project -c com.example.MyClass --no-cache
|
|
154
|
-
```
|
|
155
|
-
|
|
156
138
|
## 缓存文件
|
|
157
139
|
|
|
158
140
|
在项目目录下会生成以下缓存:
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import sys
|
|
3
2
|
import subprocess
|
|
4
3
|
import re
|
|
5
4
|
from typing import List, Optional, Dict
|
|
6
5
|
from dataclasses import dataclass
|
|
7
6
|
from ..scanner.dependency_scanner import DependencyScanner
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _debug(msg: str) -> None:
|
|
11
|
-
"""仅在 DEBUG 模式下输出到 stderr(不污染 MCP stdout JSON-RPC)"""
|
|
12
|
-
if os.environ.get("LOG_LEVEL", "DEBUG").upper() == "DEBUG":
|
|
13
|
-
print(msg, file=sys.stderr)
|
|
7
|
+
from ..logger import get_logger
|
|
14
8
|
|
|
15
9
|
|
|
16
10
|
@dataclass
|
|
@@ -51,10 +45,15 @@ class JavaClassAnalyzer:
|
|
|
51
45
|
def __init__(self):
|
|
52
46
|
self.scanner = DependencyScanner()
|
|
53
47
|
|
|
48
|
+
def _log(self, project_path=None):
|
|
49
|
+
"""获取与项目路径绑定的 logger"""
|
|
50
|
+
return get_logger(project_path or self.scanner._project_path)
|
|
51
|
+
|
|
54
52
|
def analyze_class(self, class_name: str, project_path: str) -> ClassAnalysis:
|
|
55
53
|
"""
|
|
56
54
|
分析Java类的结构信息
|
|
57
55
|
"""
|
|
56
|
+
log = self._log(project_path)
|
|
58
57
|
try:
|
|
59
58
|
# 1. 获取类文件路径
|
|
60
59
|
jar_path = self.scanner.find_jar_for_class(class_name, project_path)
|
|
@@ -66,13 +65,14 @@ class JavaClassAnalyzer:
|
|
|
66
65
|
|
|
67
66
|
return analysis
|
|
68
67
|
except Exception as e:
|
|
69
|
-
|
|
68
|
+
log.debug(f"分析类 {class_name} 失败: {e}")
|
|
70
69
|
raise e
|
|
71
70
|
|
|
72
71
|
def analyze_class_with_javap(self, jar_path: str, class_name: str) -> ClassAnalysis:
|
|
73
72
|
"""
|
|
74
73
|
使用 javap 工具分析JAR包中的类结构
|
|
75
74
|
"""
|
|
75
|
+
log = self._log()
|
|
76
76
|
try:
|
|
77
77
|
javap_cmd = self.get_javap_command()
|
|
78
78
|
|
|
@@ -91,7 +91,7 @@ class JavaClassAnalyzer:
|
|
|
91
91
|
except subprocess.TimeoutExpired:
|
|
92
92
|
raise Exception("javap分析超时")
|
|
93
93
|
except Exception as e:
|
|
94
|
-
|
|
94
|
+
log.debug(f"javap 分析失败: {e}")
|
|
95
95
|
raise Exception(f"javap 分析失败: {str(e)}")
|
|
96
96
|
|
|
97
97
|
def parse_javap_output(self, output: str, class_name: str) -> ClassAnalysis:
|
|
@@ -306,7 +306,7 @@ class JavaClassAnalyzer:
|
|
|
306
306
|
modifiers=modifiers,
|
|
307
307
|
)
|
|
308
308
|
except Exception as e:
|
|
309
|
-
|
|
309
|
+
self._log().debug(f"解析方法失败: {line}, 错误: {e}")
|
|
310
310
|
return None
|
|
311
311
|
|
|
312
312
|
def split_parameters(self, params_str: str) -> List[str]:
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import sys
|
|
3
2
|
import subprocess
|
|
4
3
|
import shutil
|
|
5
4
|
import zipfile
|
|
6
5
|
from typing import Dict
|
|
7
6
|
from ..scanner.dependency_scanner import DependencyScanner
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _debug(msg: str) -> None:
|
|
11
|
-
"""仅在 DEBUG 模式下输出到 stderr(不污染 MCP stdout JSON-RPC)"""
|
|
12
|
-
if os.environ.get("LOG_LEVEL", "DEBUG").upper() == "DEBUG":
|
|
13
|
-
print(msg, file=sys.stderr)
|
|
7
|
+
from ..logger import get_logger
|
|
14
8
|
|
|
15
9
|
|
|
16
10
|
class DecompilerService:
|
|
@@ -20,6 +14,10 @@ class DecompilerService:
|
|
|
20
14
|
self.scanner = DependencyScanner()
|
|
21
15
|
self.cfr_path = ""
|
|
22
16
|
|
|
17
|
+
def _log(self, project_path=None):
|
|
18
|
+
"""获取与项目路径绑定的 logger"""
|
|
19
|
+
return get_logger(project_path or self.scanner._project_path)
|
|
20
|
+
|
|
23
21
|
def initialize_cfr_path(self) -> None:
|
|
24
22
|
"""初始化CFR工具路径"""
|
|
25
23
|
if not self.cfr_path:
|
|
@@ -28,7 +26,7 @@ class DecompilerService:
|
|
|
28
26
|
raise Exception(
|
|
29
27
|
"未找到CFR反编译工具。请下载CFR jar包到lib目录或设置CFR_PATH环境变量"
|
|
30
28
|
)
|
|
31
|
-
|
|
29
|
+
self._log().debug(f"CFR工具路径: {self.cfr_path}")
|
|
32
30
|
|
|
33
31
|
def decompile_class(
|
|
34
32
|
self,
|
|
@@ -39,25 +37,26 @@ class DecompilerService:
|
|
|
39
37
|
"""
|
|
40
38
|
反编译指定的Java类文件
|
|
41
39
|
"""
|
|
40
|
+
log = self._log(project_path)
|
|
42
41
|
try:
|
|
43
42
|
self.initialize_cfr_path()
|
|
44
43
|
|
|
45
44
|
# 1. 检查缓存
|
|
46
45
|
cache_path = self.get_cache_path(class_name, project_path)
|
|
47
46
|
if use_cache and os.path.exists(cache_path):
|
|
48
|
-
|
|
47
|
+
log.debug(f"使用缓存的反编译结果: {cache_path}")
|
|
49
48
|
with open(cache_path, "r", encoding="utf-8") as f:
|
|
50
49
|
return f.read()
|
|
51
50
|
|
|
52
51
|
# 2. 查找类对应的JAR包
|
|
53
|
-
|
|
52
|
+
log.debug(f"查找类 {class_name} 对应的JAR包...")
|
|
54
53
|
jar_path = self.scanner.find_jar_for_class(class_name, project_path)
|
|
55
54
|
|
|
56
55
|
if not jar_path:
|
|
57
56
|
raise Exception(
|
|
58
57
|
f"未找到类 {class_name} 对应的JAR包,请先运行 scan_dependencies 建立类索引"
|
|
59
58
|
)
|
|
60
|
-
|
|
59
|
+
log.debug(f"找到JAR包: {jar_path}")
|
|
61
60
|
|
|
62
61
|
# 3. 从JAR包中提取.class文件
|
|
63
62
|
class_file_path = self.extract_class_file(jar_path, class_name)
|
|
@@ -70,19 +69,19 @@ class DecompilerService:
|
|
|
70
69
|
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
|
|
71
70
|
with open(cache_path, "w", encoding="utf-8") as f:
|
|
72
71
|
f.write(source_code)
|
|
73
|
-
|
|
72
|
+
log.debug(f"反编译结果已缓存: {cache_path}")
|
|
74
73
|
|
|
75
74
|
# 6. 清理临时文件(只有在不使用缓存时才清理)
|
|
76
75
|
if not use_cache:
|
|
77
76
|
try:
|
|
78
77
|
os.remove(class_file_path)
|
|
79
|
-
|
|
78
|
+
log.debug(f"清理临时文件: {class_file_path}")
|
|
80
79
|
except Exception as e:
|
|
81
|
-
|
|
80
|
+
log.debug(f"清理临时文件失败: {e}")
|
|
82
81
|
|
|
83
82
|
return source_code
|
|
84
83
|
except Exception as e:
|
|
85
|
-
|
|
84
|
+
log.debug(f"反编译类 {class_name} 失败: {e}")
|
|
86
85
|
raise e
|
|
87
86
|
|
|
88
87
|
def get_cache_path(self, class_name: str, project_path: str) -> str:
|
|
@@ -99,6 +98,7 @@ class DecompilerService:
|
|
|
99
98
|
"""
|
|
100
99
|
从JAR包中提取指定的.class文件
|
|
101
100
|
"""
|
|
101
|
+
log = self._log()
|
|
102
102
|
class_file_name = class_name.replace(".", "/") + ".class"
|
|
103
103
|
temp_dir = os.path.join(os.getcwd(), ".mcp-class-temp")
|
|
104
104
|
# 按包名全路径创建目录结构
|
|
@@ -110,7 +110,7 @@ class DecompilerService:
|
|
|
110
110
|
|
|
111
111
|
os.makedirs(package_dir, exist_ok=True)
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
log.debug(f"从JAR包提取类文件: {jar_path} -> {class_file_name}")
|
|
114
114
|
|
|
115
115
|
with zipfile.ZipFile(jar_path, "r") as zf:
|
|
116
116
|
try:
|
|
@@ -119,7 +119,7 @@ class DecompilerService:
|
|
|
119
119
|
open(class_file_path, "wb") as target,
|
|
120
120
|
):
|
|
121
121
|
shutil.copyfileobj(source, target)
|
|
122
|
-
|
|
122
|
+
log.debug(f"类文件提取成功: {class_file_path}")
|
|
123
123
|
return class_file_path
|
|
124
124
|
except KeyError:
|
|
125
125
|
raise Exception(f"在JAR包 {jar_path} 中未找到类文件: {class_file_name}")
|
|
@@ -128,12 +128,13 @@ class DecompilerService:
|
|
|
128
128
|
"""
|
|
129
129
|
使用CFR反编译.class文件
|
|
130
130
|
"""
|
|
131
|
+
log = self._log()
|
|
131
132
|
if not self.cfr_path:
|
|
132
133
|
raise Exception("未找到CFR反编译工具,请确保CFR jar包在classpath中")
|
|
133
134
|
|
|
134
135
|
try:
|
|
135
136
|
java_cmd = self.get_java_command()
|
|
136
|
-
|
|
137
|
+
log.debug(
|
|
137
138
|
f'执行CFR反编译: {java_cmd} -jar "{self.cfr_path}" "{class_file_path}"'
|
|
138
139
|
)
|
|
139
140
|
|
|
@@ -145,7 +146,7 @@ class DecompilerService:
|
|
|
145
146
|
)
|
|
146
147
|
|
|
147
148
|
if result.stderr and result.stderr.strip():
|
|
148
|
-
|
|
149
|
+
log.debug(f"CFR警告: {result.stderr}")
|
|
149
150
|
|
|
150
151
|
if not result.stdout or result.stdout.strip() == "":
|
|
151
152
|
raise Exception("CFR反编译返回空结果,可能是类文件损坏或CFR版本不兼容")
|
|
@@ -154,7 +155,7 @@ class DecompilerService:
|
|
|
154
155
|
except subprocess.TimeoutExpired:
|
|
155
156
|
raise Exception("CFR反编译超时,请检查Java环境和CFR工具")
|
|
156
157
|
except Exception as e:
|
|
157
|
-
|
|
158
|
+
log.debug(f"CFR反编译执行失败: {e}")
|
|
158
159
|
raise Exception(f"CFR反编译失败: {str(e)}")
|
|
159
160
|
|
|
160
161
|
def find_cfr_jar(self) -> str:
|
|
@@ -162,14 +163,15 @@ class DecompilerService:
|
|
|
162
163
|
查找CFR jar包路径。
|
|
163
164
|
优先级:① CFR_PATH 环境变量(用户配置)→ ② 包内置 jar(随 pip 安装分发)
|
|
164
165
|
"""
|
|
166
|
+
log = self._log()
|
|
165
167
|
# ① 优先使用用户通过环境变量指定的路径
|
|
166
168
|
cfr_path = os.environ.get("CFR_PATH", "").strip()
|
|
167
169
|
if cfr_path:
|
|
168
170
|
if os.path.isfile(cfr_path):
|
|
169
|
-
|
|
171
|
+
log.debug(f"使用 CFR_PATH 环境变量指定的路径: {cfr_path}")
|
|
170
172
|
return cfr_path
|
|
171
173
|
else:
|
|
172
|
-
|
|
174
|
+
log.debug(f"CFR_PATH 指定的文件不存在: {cfr_path},继续查找内置 jar")
|
|
173
175
|
|
|
174
176
|
# ② 使用包内置的 cfr jar(通过 importlib.resources 获取,兼容 pip 安装后的路径)
|
|
175
177
|
try:
|
|
@@ -179,10 +181,10 @@ class DecompilerService:
|
|
|
179
181
|
for fname in os.listdir(pkg_dir):
|
|
180
182
|
if fname.lower().startswith("cfr") and fname.endswith(".jar"):
|
|
181
183
|
found = os.path.join(pkg_dir, fname)
|
|
182
|
-
|
|
184
|
+
log.debug(f"找到内置 CFR jar: {found}")
|
|
183
185
|
return found
|
|
184
186
|
except Exception as e:
|
|
185
|
-
|
|
187
|
+
log.debug(f"查找内置 CFR jar 失败: {e}")
|
|
186
188
|
|
|
187
189
|
return ""
|
|
188
190
|
|
|
@@ -195,6 +197,7 @@ class DecompilerService:
|
|
|
195
197
|
"""
|
|
196
198
|
批量反编译多个类
|
|
197
199
|
"""
|
|
200
|
+
log = self._log(project_path)
|
|
198
201
|
results = {}
|
|
199
202
|
|
|
200
203
|
for class_name in class_names:
|
|
@@ -202,7 +205,7 @@ class DecompilerService:
|
|
|
202
205
|
source_code = self.decompile_class(class_name, project_path, use_cache)
|
|
203
206
|
results[class_name] = source_code
|
|
204
207
|
except Exception as e:
|
|
205
|
-
|
|
208
|
+
log.debug(f"反编译类 {class_name} 失败: {e}")
|
|
206
209
|
results[class_name] = f"// 反编译失败: {e}"
|
|
207
210
|
|
|
208
211
|
return results
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
统一日志模块
|
|
3
|
+
|
|
4
|
+
日志同时输出到:
|
|
5
|
+
1. stderr(不污染 MCP stdout JSON-RPC)
|
|
6
|
+
2. <projectPath>/.java-class-analyzer/logs/YYYY-MM-DD.log(按天滚动)
|
|
7
|
+
|
|
8
|
+
用法:
|
|
9
|
+
from .logger import get_logger
|
|
10
|
+
logger = get_logger(project_path) # project_path 可为 None
|
|
11
|
+
logger.debug("...")
|
|
12
|
+
logger.info("...")
|
|
13
|
+
logger.warning("...")
|
|
14
|
+
logger.error("...")
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from logging.handlers import TimedRotatingFileHandler
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
_LOG_DIR_NAME = ".java-class-analyzer"
|
|
24
|
+
_LOG_SUBDIR = "logs"
|
|
25
|
+
|
|
26
|
+
# 全局 logger 缓存,key = project_path(或 "" 表示无项目路径)
|
|
27
|
+
_loggers: dict[str, logging.Logger] = {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_debug() -> bool:
|
|
31
|
+
return os.environ.get("LOG_LEVEL", "DEBUG").upper() == "DEBUG"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _build_logger(name: str, log_file: Optional[str]) -> logging.Logger:
|
|
35
|
+
"""构造一个 Logger 实例(stderr + 可选的文件处理器)"""
|
|
36
|
+
logger = logging.getLogger(name)
|
|
37
|
+
# 避免重复添加 handler
|
|
38
|
+
if logger.handlers:
|
|
39
|
+
return logger
|
|
40
|
+
|
|
41
|
+
logger.setLevel(logging.DEBUG if _is_debug() else logging.INFO)
|
|
42
|
+
logger.propagate = False
|
|
43
|
+
|
|
44
|
+
fmt = logging.Formatter(
|
|
45
|
+
fmt="%(asctime)s [%(levelname)s] %(message)s",
|
|
46
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Handler 1: stderr
|
|
50
|
+
stderr_handler = logging.StreamHandler(sys.stderr)
|
|
51
|
+
stderr_handler.setLevel(logging.DEBUG)
|
|
52
|
+
stderr_handler.setFormatter(fmt)
|
|
53
|
+
logger.addHandler(stderr_handler)
|
|
54
|
+
|
|
55
|
+
# Handler 2: 按天滚动的文件日志
|
|
56
|
+
if log_file:
|
|
57
|
+
try:
|
|
58
|
+
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
|
59
|
+
file_handler = TimedRotatingFileHandler(
|
|
60
|
+
filename=log_file,
|
|
61
|
+
when="midnight", # 每天零点切割
|
|
62
|
+
interval=1,
|
|
63
|
+
backupCount=30, # 保留最近 30 天
|
|
64
|
+
encoding="utf-8",
|
|
65
|
+
utc=False,
|
|
66
|
+
)
|
|
67
|
+
# 切割后的文件名后缀格式:YYYY-MM-DD
|
|
68
|
+
file_handler.suffix = "%Y-%m-%d"
|
|
69
|
+
file_handler.setLevel(logging.DEBUG)
|
|
70
|
+
file_handler.setFormatter(fmt)
|
|
71
|
+
logger.addHandler(file_handler)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
# 文件日志初始化失败不影响程序运行
|
|
74
|
+
print(f"[logger] 初始化文件日志失败: {e}", file=sys.stderr)
|
|
75
|
+
|
|
76
|
+
return logger
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_logger(project_path: Optional[str] = None) -> logging.Logger:
|
|
80
|
+
"""
|
|
81
|
+
获取(或创建)与 project_path 绑定的 Logger。
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
project_path: Maven 项目根目录路径。为 None 时只写 stderr,不写文件。
|
|
85
|
+
"""
|
|
86
|
+
key = project_path or ""
|
|
87
|
+
if key in _loggers:
|
|
88
|
+
return _loggers[key]
|
|
89
|
+
|
|
90
|
+
log_file: Optional[str] = None
|
|
91
|
+
if project_path:
|
|
92
|
+
log_dir = os.path.join(project_path, _LOG_DIR_NAME, _LOG_SUBDIR)
|
|
93
|
+
# 文件名使用当天日期(TimedRotatingFileHandler 会自动按天切割)
|
|
94
|
+
log_file = os.path.join(log_dir, "java-class-analyzer.log")
|
|
95
|
+
|
|
96
|
+
logger_name = f"java_class_analyzer.{key}" if key else "java_class_analyzer"
|
|
97
|
+
logger = _build_logger(logger_name, log_file)
|
|
98
|
+
_loggers[key] = logger
|
|
99
|
+
return logger
|
{java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp/main.py
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import sys
|
|
5
4
|
|
|
6
5
|
from .analyzer.java_class_analyzer import JavaClassAnalyzer
|
|
7
6
|
from .scanner.dependency_scanner import DependencyScanner
|
|
8
7
|
from .decompiler.decompiler_service import DecompilerService
|
|
8
|
+
from .logger import get_logger
|
|
9
9
|
|
|
10
10
|
from mcp.server.fastmcp import FastMCP
|
|
11
11
|
|
|
@@ -15,6 +15,9 @@ _scanner = DependencyScanner()
|
|
|
15
15
|
_decompiler = DecompilerService()
|
|
16
16
|
_analyzer = JavaClassAnalyzer()
|
|
17
17
|
|
|
18
|
+
# 全局 logger(不绑定项目路径,仅写 stderr;工具调用时会按项目路径再获取对应 logger)
|
|
19
|
+
_log = get_logger()
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
def _ensure_index(project_path: str) -> None:
|
|
20
23
|
"""确保索引文件存在,不存在则自动创建"""
|
|
@@ -31,8 +34,15 @@ def scan_dependencies(projectPath: str, forceRefresh: bool = False) -> str:
|
|
|
31
34
|
projectPath: Maven项目根目录路径
|
|
32
35
|
forceRefresh: 是否强制刷新索引,默认false
|
|
33
36
|
"""
|
|
37
|
+
log = get_logger(projectPath)
|
|
38
|
+
log.info(
|
|
39
|
+
f"scan_dependencies 开始: projectPath={projectPath}, forceRefresh={forceRefresh}"
|
|
40
|
+
)
|
|
34
41
|
result = _scanner.scan_project(projectPath, forceRefresh)
|
|
35
42
|
sample = "\n".join(result.sample_entries[:5])
|
|
43
|
+
log.info(
|
|
44
|
+
f"scan_dependencies 完成: jarCount={result.jar_count}, classCount={result.class_count}"
|
|
45
|
+
)
|
|
36
46
|
return (
|
|
37
47
|
f"依赖扫描完成!\n\n"
|
|
38
48
|
f"扫描的JAR包数量: {result.jar_count}\n"
|
|
@@ -55,10 +65,14 @@ def decompile_class(
|
|
|
55
65
|
projectPath: Maven项目根目录路径
|
|
56
66
|
useCache: 是否使用缓存,默认true
|
|
57
67
|
"""
|
|
68
|
+
log = get_logger(projectPath)
|
|
69
|
+
log.info(f"decompile_class 开始: className={className}, projectPath={projectPath}")
|
|
58
70
|
_ensure_index(projectPath)
|
|
59
71
|
source_code = _decompiler.decompile_class(className, projectPath, useCache)
|
|
60
72
|
if not source_code or not source_code.strip():
|
|
73
|
+
log.warning(f"decompile_class 结果为空: className={className}")
|
|
61
74
|
return f"警告: 类 {className} 的反编译结果为空,可能是CFR工具问题或类文件损坏"
|
|
75
|
+
log.info(f"decompile_class 完成: className={className}")
|
|
62
76
|
return f"类 {className} 的反编译源码:\n\n```java\n{source_code}\n```"
|
|
63
77
|
|
|
64
78
|
|
|
@@ -70,6 +84,8 @@ def analyze_class(className: str, projectPath: str) -> str:
|
|
|
70
84
|
className: 要分析的Java类全名
|
|
71
85
|
projectPath: Maven项目根目录路径
|
|
72
86
|
"""
|
|
87
|
+
log = get_logger(projectPath)
|
|
88
|
+
log.info(f"analyze_class 开始: className={className}, projectPath={projectPath}")
|
|
73
89
|
_ensure_index(projectPath)
|
|
74
90
|
analysis = _analyzer.analyze_class(className, projectPath)
|
|
75
91
|
|
|
@@ -91,10 +107,12 @@ def analyze_class(className: str, projectPath: str) -> str:
|
|
|
91
107
|
for method in analysis.methods:
|
|
92
108
|
result += f" - {' '.join(method.modifiers)} {method.return_type} {method.name}({', '.join(method.parameters)})\n"
|
|
93
109
|
|
|
110
|
+
log.info(f"analyze_class 完成: className={className}")
|
|
94
111
|
return result
|
|
95
112
|
|
|
96
113
|
|
|
97
114
|
def main():
|
|
115
|
+
_log.info("java-class-analyzer MCP server 启动")
|
|
98
116
|
mcp.run(transport="stdio")
|
|
99
117
|
|
|
100
118
|
|
|
@@ -1,25 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import sys
|
|
3
2
|
import subprocess
|
|
4
3
|
import json
|
|
5
4
|
import zipfile
|
|
6
5
|
from typing import List, Dict, Optional, Set
|
|
7
6
|
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _is_debug() -> bool:
|
|
11
|
-
return os.environ.get("LOG_LEVEL", "DEBUG").upper() == "DEBUG"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _log(msg: str) -> None:
|
|
15
|
-
"""仅在 DEBUG 模式下输出到 stderr(不污染 MCP stdout)"""
|
|
16
|
-
if _is_debug():
|
|
17
|
-
print(msg, file=sys.stderr)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _log_always(msg: str) -> None:
|
|
21
|
-
"""始终输出到 stderr(不污染 MCP stdout)"""
|
|
22
|
-
print(msg, file=sys.stderr)
|
|
7
|
+
from ..logger import get_logger
|
|
23
8
|
|
|
24
9
|
|
|
25
10
|
class ClassIndexEntry:
|
|
@@ -55,6 +40,13 @@ class DependencyScanner:
|
|
|
55
40
|
|
|
56
41
|
def __init__(self):
|
|
57
42
|
self.index_cache: Dict[str, List[ClassIndexEntry]] = {}
|
|
43
|
+
# logger 在首次使用项目路径时通过 _logger(project_path) 懒加载
|
|
44
|
+
self._project_path: Optional[str] = None
|
|
45
|
+
|
|
46
|
+
def _logger(self, project_path: Optional[str] = None):
|
|
47
|
+
"""获取与当前项目路径绑定的 logger"""
|
|
48
|
+
path = project_path or self._project_path
|
|
49
|
+
return get_logger(path)
|
|
58
50
|
|
|
59
51
|
def get_package_prefixes(self) -> List[str]:
|
|
60
52
|
"""
|
|
@@ -73,16 +65,18 @@ class DependencyScanner:
|
|
|
73
65
|
"""
|
|
74
66
|
扫描Maven项目的所有依赖,建立类名到JAR包的映射索引
|
|
75
67
|
"""
|
|
68
|
+
self._project_path = project_path
|
|
69
|
+
log = self._logger(project_path)
|
|
76
70
|
index_path = os.path.join(project_path, ".mcp-class-index.json")
|
|
77
71
|
|
|
78
72
|
# 如果强制刷新,先删除旧的索引文件
|
|
79
73
|
if force_refresh and os.path.exists(index_path):
|
|
80
|
-
|
|
74
|
+
log.debug("强制刷新:删除旧的索引文件")
|
|
81
75
|
os.remove(index_path)
|
|
82
76
|
|
|
83
77
|
# 检查缓存
|
|
84
78
|
if not force_refresh and os.path.exists(index_path):
|
|
85
|
-
|
|
79
|
+
log.debug("使用缓存的类索引")
|
|
86
80
|
with open(index_path, "r", encoding="utf-8") as f:
|
|
87
81
|
cached_index = json.load(f)
|
|
88
82
|
return ScanResult(
|
|
@@ -92,22 +86,22 @@ class DependencyScanner:
|
|
|
92
86
|
cached_index["sampleEntries"],
|
|
93
87
|
)
|
|
94
88
|
|
|
95
|
-
|
|
89
|
+
log.debug("开始扫描Maven依赖...")
|
|
96
90
|
|
|
97
91
|
# 读取包名前缀过滤配置
|
|
98
92
|
package_prefixes = self.get_package_prefixes()
|
|
99
93
|
if package_prefixes:
|
|
100
|
-
|
|
94
|
+
log.info(
|
|
101
95
|
f"包名前缀过滤已启用,只索引以下前缀的类: {', '.join(package_prefixes)}"
|
|
102
96
|
)
|
|
103
97
|
else:
|
|
104
|
-
|
|
98
|
+
log.debug(
|
|
105
99
|
"包名前缀过滤未配置,将索引所有类(可通过 CLASS_PACKAGE_PREFIXES 环境变量限制)"
|
|
106
100
|
)
|
|
107
101
|
|
|
108
102
|
# 1. 获取Maven依赖树
|
|
109
103
|
dependencies = self.get_maven_dependencies(project_path)
|
|
110
|
-
|
|
104
|
+
log.debug(f"找到 {len(dependencies)} 个依赖JAR包")
|
|
111
105
|
|
|
112
106
|
# 2. 解析每个JAR包,建立类索引
|
|
113
107
|
class_index: List[ClassIndexEntry] = []
|
|
@@ -120,9 +114,9 @@ class DependencyScanner:
|
|
|
120
114
|
processed_jars += 1
|
|
121
115
|
|
|
122
116
|
if processed_jars % 10 == 0:
|
|
123
|
-
|
|
117
|
+
log.debug(f"已处理 {processed_jars}/{len(dependencies)} 个JAR包")
|
|
124
118
|
except Exception as e:
|
|
125
|
-
|
|
119
|
+
log.debug(f"处理JAR包失败: {jar_path}, 错误: {e}")
|
|
126
120
|
|
|
127
121
|
# 3. 保存索引到文件
|
|
128
122
|
sample_entries = [
|
|
@@ -155,7 +149,7 @@ class DependencyScanner:
|
|
|
155
149
|
with open(index_path, "w", encoding="utf-8") as f:
|
|
156
150
|
json.dump(index_data, f, indent=2, ensure_ascii=False)
|
|
157
151
|
|
|
158
|
-
|
|
152
|
+
log.debug(
|
|
159
153
|
f"扫描完成!处理了 {processed_jars} 个JAR包,索引了 {len(class_index)} 个类"
|
|
160
154
|
)
|
|
161
155
|
|
|
@@ -165,6 +159,7 @@ class DependencyScanner:
|
|
|
165
159
|
"""
|
|
166
160
|
获取Maven依赖树中的所有JAR包路径
|
|
167
161
|
"""
|
|
162
|
+
log = self._logger(project_path)
|
|
168
163
|
try:
|
|
169
164
|
# 构建Maven命令路径
|
|
170
165
|
maven_cmd = self.get_maven_command()
|
|
@@ -201,7 +196,7 @@ class DependencyScanner:
|
|
|
201
196
|
|
|
202
197
|
return list(jar_paths)
|
|
203
198
|
except Exception as e:
|
|
204
|
-
|
|
199
|
+
log.debug(f"获取Maven依赖失败: {e}")
|
|
205
200
|
# 如果Maven命令失败,尝试从本地仓库扫描
|
|
206
201
|
return self.scan_local_maven_repo(project_path)
|
|
207
202
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: java-class-analyzer-mcp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: MCP server for scanning Maven dependencies and decompiling Java classes
|
|
5
5
|
Author: java-class-analyzer-mcp contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -150,24 +150,6 @@ java-class-analyzer-mcp/
|
|
|
150
150
|
- `className` (string): 要分析的类全名
|
|
151
151
|
- `projectPath` (string): Maven 项目根目录路径
|
|
152
152
|
|
|
153
|
-
## 命令行工具
|
|
154
|
-
|
|
155
|
-
安装后可直接使用命令行工具:
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
# 启动 MCP 服务器(stdio 模式,供 MCP 客户端调用)
|
|
159
|
-
java-class-analyzer-mcp
|
|
160
|
-
|
|
161
|
-
# 生成 MCP 配置模板
|
|
162
|
-
java-class-analyzer-mcp config -o mcp-config.json
|
|
163
|
-
|
|
164
|
-
# 测试工具
|
|
165
|
-
java-class-analyzer-mcp test -t scan -p /path/to/project
|
|
166
|
-
java-class-analyzer-mcp test -t decompile -p /path/to/project -c com.example.MyClass
|
|
167
|
-
java-class-analyzer-mcp test -t analyze -p /path/to/project -c com.example.MyClass
|
|
168
|
-
java-class-analyzer-mcp test -t all -p /path/to/project -c com.example.MyClass --no-cache
|
|
169
|
-
```
|
|
170
|
-
|
|
171
153
|
## 缓存文件
|
|
172
154
|
|
|
173
155
|
在项目目录下会生成以下缓存:
|
|
@@ -3,6 +3,7 @@ pyproject.toml
|
|
|
3
3
|
java_class_analyzer_mcp/__init__.py
|
|
4
4
|
java_class_analyzer_mcp/cfr-0.152.jar
|
|
5
5
|
java_class_analyzer_mcp/cli.py
|
|
6
|
+
java_class_analyzer_mcp/logger.py
|
|
6
7
|
java_class_analyzer_mcp/main.py
|
|
7
8
|
java_class_analyzer_mcp.egg-info/PKG-INFO
|
|
8
9
|
java_class_analyzer_mcp.egg-info/SOURCES.txt
|
{java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_class_analyzer_mcp-0.1.1 → java_class_analyzer_mcp-0.1.2}/java_class_analyzer_mcp/cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|