java-class-analyzer-mcp 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,3 @@
1
+ """
2
+ Java Class Analyzer MCP Server (Python版)
3
+ """
@@ -0,0 +1,3 @@
1
+ """
2
+ Java类结构分析模块
3
+ """
@@ -0,0 +1,384 @@
1
+ import os
2
+ import sys
3
+ import subprocess
4
+ import re
5
+ from typing import List, Optional, Dict
6
+ from dataclasses import dataclass
7
+ 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)
14
+
15
+
16
+ @dataclass
17
+ class ClassField:
18
+ """类字段"""
19
+
20
+ name: str
21
+ type_: str
22
+ modifiers: List[str]
23
+
24
+
25
+ @dataclass
26
+ class ClassMethod:
27
+ """类方法"""
28
+
29
+ name: str
30
+ return_type: str
31
+ parameters: List[str]
32
+ modifiers: List[str]
33
+
34
+
35
+ @dataclass
36
+ class ClassAnalysis:
37
+ """类分析结果"""
38
+
39
+ class_name: str
40
+ package_name: str
41
+ modifiers: List[str]
42
+ super_class: Optional[str]
43
+ interfaces: List[str]
44
+ fields: List[ClassField]
45
+ methods: List[ClassMethod]
46
+
47
+
48
+ class JavaClassAnalyzer:
49
+ """Java类分析器"""
50
+
51
+ def __init__(self):
52
+ self.scanner = DependencyScanner()
53
+
54
+ def analyze_class(self, class_name: str, project_path: str) -> ClassAnalysis:
55
+ """
56
+ 分析Java类的结构信息
57
+ """
58
+ try:
59
+ # 1. 获取类文件路径
60
+ jar_path = self.scanner.find_jar_for_class(class_name, project_path)
61
+ if not jar_path:
62
+ raise Exception(f"未找到类 {class_name} 对应的JAR包")
63
+
64
+ # 2. 直接使用 javap 分析JAR包中的类
65
+ analysis = self.analyze_class_with_javap(jar_path, class_name)
66
+
67
+ return analysis
68
+ except Exception as e:
69
+ _debug(f"分析类 {class_name} 失败: {e}")
70
+ raise e
71
+
72
+ def analyze_class_with_javap(self, jar_path: str, class_name: str) -> ClassAnalysis:
73
+ """
74
+ 使用 javap 工具分析JAR包中的类结构
75
+ """
76
+ try:
77
+ javap_cmd = self.get_javap_command()
78
+
79
+ # 使用 javap -v 获取详细信息(包括参数名称)
80
+ result = subprocess.run(
81
+ [javap_cmd, "-v", "-cp", jar_path, class_name],
82
+ capture_output=True,
83
+ text=True,
84
+ timeout=10,
85
+ )
86
+
87
+ if result.returncode != 0:
88
+ raise Exception(f"javap执行失败: {result.stderr}")
89
+
90
+ return self.parse_javap_output(result.stdout, class_name)
91
+ except subprocess.TimeoutExpired:
92
+ raise Exception("javap分析超时")
93
+ except Exception as e:
94
+ _debug(f"javap 分析失败: {e}")
95
+ raise Exception(f"javap 分析失败: {str(e)}")
96
+
97
+ def parse_javap_output(self, output: str, class_name: str) -> ClassAnalysis:
98
+ """
99
+ 解析 javap 输出
100
+ """
101
+ lines = output.split("\n")
102
+ # 从类全名中提取简单类名
103
+ simple_class_name = class_name.split(".")[-1]
104
+ package_name = ".".join(class_name.split(".")[:-1]) if "." in class_name else ""
105
+
106
+ analysis = ClassAnalysis(
107
+ class_name=simple_class_name,
108
+ package_name=package_name,
109
+ modifiers=[],
110
+ super_class=None,
111
+ interfaces=[],
112
+ fields=[],
113
+ methods=[],
114
+ )
115
+
116
+ current_method = None
117
+ in_local_variable_table = False
118
+ method_parameters = {}
119
+
120
+ for i, line in enumerate(lines):
121
+ trimmed_line = line.strip()
122
+
123
+ # 解析类声明
124
+ if any(
125
+ trimmed_line.startswith(prefix)
126
+ for prefix in ["public class", "public interface", "public enum"]
127
+ ):
128
+ self.parse_class_declaration(trimmed_line, analysis)
129
+ continue
130
+
131
+ # 解析方法声明
132
+ if (
133
+ (
134
+ "public " in trimmed_line
135
+ or "private " in trimmed_line
136
+ or "protected " in trimmed_line
137
+ )
138
+ and "(" in trimmed_line
139
+ and ")" in trimmed_line
140
+ ):
141
+ current_method = self.parse_method_from_javap(trimmed_line)
142
+ if current_method:
143
+ analysis.methods.append(current_method)
144
+ method_parameters = {}
145
+ continue
146
+
147
+ # 检测 LocalVariableTable 开始
148
+ if trimmed_line == "LocalVariableTable:":
149
+ in_local_variable_table = True
150
+ continue
151
+
152
+ # 解析 LocalVariableTable 中的参数名称
153
+ if in_local_variable_table and current_method:
154
+ if trimmed_line.startswith("Start") or trimmed_line.startswith("Slot"):
155
+ continue # 跳过表头
156
+
157
+ if trimmed_line == "":
158
+ # LocalVariableTable 结束,立即更新当前方法的参数名称
159
+ if method_parameters:
160
+ updated_params = []
161
+ for j, param_type in enumerate(current_method.parameters):
162
+ param_name = method_parameters.get(j, f"param{j + 1}")
163
+ updated_params.append(f"{param_type} {param_name}")
164
+ current_method.parameters = updated_params
165
+ in_local_variable_table = False
166
+ method_parameters = {}
167
+ continue
168
+
169
+ # 解析参数行: "0 6 0 file Ljava/io/File;"
170
+ param_match = re.match(
171
+ r"^\s*(\d+)\s+\d+\s+(\d+)\s+(\w+)\s+(.+)$", trimmed_line
172
+ )
173
+ if param_match:
174
+ slot = int(param_match.group(2))
175
+ param_name = param_match.group(3)
176
+ param_type = param_match.group(4)
177
+
178
+ # 只处理参数(slot >= 0,但排除局部变量)
179
+ if slot >= 0 and slot < len(current_method.parameters):
180
+ method_parameters[slot] = param_name
181
+
182
+ # 检测方法结束 - 当遇到下一个方法或类结束时
183
+ if current_method and (
184
+ (
185
+ (
186
+ "public " in trimmed_line
187
+ or "private " in trimmed_line
188
+ or "protected " in trimmed_line
189
+ )
190
+ and "(" in trimmed_line
191
+ and ")" in trimmed_line
192
+ )
193
+ or trimmed_line.startswith("}")
194
+ or trimmed_line.startswith("SourceFile:")
195
+ ):
196
+ # 更新方法的参数名称
197
+ if method_parameters:
198
+ updated_params = []
199
+ for j, param_type in enumerate(current_method.parameters):
200
+ param_name = method_parameters.get(j, f"param{j + 1}")
201
+ updated_params.append(f"{param_type} {param_name}")
202
+ current_method.parameters = updated_params
203
+ current_method = None
204
+ in_local_variable_table = False
205
+ method_parameters = {}
206
+
207
+ return analysis
208
+
209
+ def parse_class_declaration(self, line: str, analysis: ClassAnalysis) -> None:
210
+ """
211
+ 解析类声明
212
+ """
213
+ # 提取修饰符
214
+ modifiers = re.findall(
215
+ r"\b(public|private|protected|static|final|abstract|strictfp)\b", line
216
+ )
217
+ analysis.modifiers = modifiers
218
+
219
+ # 提取父类
220
+ extends_match = re.search(r"extends\s+([a-zA-Z_$][a-zA-Z0-9_$.]*)", line)
221
+ if extends_match:
222
+ analysis.super_class = extends_match.group(1)
223
+
224
+ # 提取接口
225
+ implements_match = re.search(r"implements\s+([^{]+)", line)
226
+ if implements_match:
227
+ interfaces = [
228
+ iface.strip()
229
+ for iface in implements_match.group(1).split(",")
230
+ if iface.strip()
231
+ ]
232
+ analysis.interfaces = interfaces
233
+
234
+ def parse_method_from_javap(self, line: str) -> Optional[ClassMethod]:
235
+ """
236
+ 从 javap 输出解析方法
237
+ """
238
+ try:
239
+ trimmed_line = line.strip()
240
+
241
+ # 提取修饰符
242
+ modifiers = []
243
+ modifier_words = [
244
+ "public",
245
+ "private",
246
+ "protected",
247
+ "static",
248
+ "final",
249
+ "abstract",
250
+ "synchronized",
251
+ "native",
252
+ ]
253
+
254
+ # 处理多个修饰符
255
+ remaining_line = trimmed_line
256
+ while True:
257
+ found_modifier = False
258
+ for modifier in modifier_words:
259
+ if remaining_line.startswith(modifier + " "):
260
+ modifiers.append(modifier)
261
+ remaining_line = remaining_line[len(modifier) + 1 :]
262
+ found_modifier = True
263
+ break
264
+ if not found_modifier:
265
+ break
266
+
267
+ # 查找方法名和参数部分
268
+ paren_index = trimmed_line.find("(")
269
+ if paren_index == -1:
270
+ return None
271
+
272
+ close_paren_index = trimmed_line.find(")", paren_index)
273
+ if close_paren_index == -1:
274
+ return None
275
+
276
+ # 提取返回类型和方法名
277
+ before_paren = (
278
+ trimmed_line[
279
+ trimmed_line.find(modifiers[-1])
280
+ + len(modifiers[-1])
281
+ + 1 : paren_index
282
+ ].strip()
283
+ if modifiers
284
+ else trimmed_line[:paren_index].strip()
285
+ )
286
+ last_space_index = before_paren.rfind(" ")
287
+ if last_space_index == -1:
288
+ return None
289
+
290
+ return_type = before_paren[:last_space_index].strip()
291
+ method_name = before_paren[last_space_index + 1 :].strip()
292
+
293
+ # 提取参数
294
+ params_str = trimmed_line[paren_index + 1 : close_paren_index].strip()
295
+ parameters = []
296
+
297
+ if params_str:
298
+ # 处理参数,需要考虑泛型和嵌套类型
299
+ param_parts = self.split_parameters(params_str)
300
+ parameters = [param.strip() for param in param_parts if param.strip()]
301
+
302
+ return ClassMethod(
303
+ name=method_name,
304
+ return_type=return_type,
305
+ parameters=parameters,
306
+ modifiers=modifiers,
307
+ )
308
+ except Exception as e:
309
+ _debug(f"解析方法失败: {line}, 错误: {e}")
310
+ return None
311
+
312
+ def split_parameters(self, params_str: str) -> List[str]:
313
+ """
314
+ 智能分割参数,处理泛型和嵌套类型
315
+ """
316
+ params = []
317
+ current = ""
318
+ angle_bracket_count = 0
319
+
320
+ for char in params_str:
321
+ if char == "<":
322
+ angle_bracket_count += 1
323
+ elif char == ">":
324
+ angle_bracket_count -= 1
325
+ elif char == "," and angle_bracket_count == 0:
326
+ params.append(current)
327
+ current = ""
328
+ continue
329
+
330
+ current += char
331
+
332
+ if current.strip():
333
+ params.append(current)
334
+
335
+ return params
336
+
337
+ def get_javap_command(self) -> str:
338
+ """
339
+ 获取javap命令路径
340
+ """
341
+ java_home = os.environ.get("JAVA_HOME")
342
+ if java_home:
343
+ javap_cmd = "javap.exe" if os.name == "nt" else "javap"
344
+ return os.path.join(java_home, "bin", javap_cmd)
345
+ return "javap"
346
+
347
+ def get_inheritance_hierarchy(
348
+ self, class_name: str, project_path: str
349
+ ) -> List[str]:
350
+ """
351
+ 获取类的继承层次结构
352
+ """
353
+ analysis = self.analyze_class(class_name, project_path)
354
+ hierarchy = [class_name]
355
+
356
+ if analysis.super_class:
357
+ try:
358
+ super_hierarchy = self.get_inheritance_hierarchy(
359
+ analysis.super_class, project_path
360
+ )
361
+ hierarchy = super_hierarchy + hierarchy
362
+ except Exception:
363
+ # 如果父类不在当前项目中,直接添加
364
+ hierarchy.insert(0, analysis.super_class)
365
+
366
+ return hierarchy
367
+
368
+ def find_sub_classes(self, class_name: str, project_path: str) -> List[str]:
369
+ """
370
+ 查找类的所有子类
371
+ """
372
+ all_classes = self.scanner.get_all_class_names(project_path)
373
+ sub_classes = []
374
+
375
+ for cls in all_classes:
376
+ try:
377
+ analysis = self.analyze_class(cls, project_path)
378
+ if analysis.super_class == class_name:
379
+ sub_classes.append(cls)
380
+ except Exception:
381
+ # 忽略分析失败的类型
382
+ pass
383
+
384
+ return sub_classes
Binary file
@@ -0,0 +1,3 @@
1
+ """
2
+ 命令行工具模块
3
+ """
@@ -0,0 +1,145 @@
1
+ import click
2
+ import json
3
+ import os
4
+ import sys
5
+ from .scanner.dependency_scanner import DependencyScanner
6
+ from .decompiler.decompiler_service import DecompilerService
7
+ from .analyzer.java_class_analyzer import JavaClassAnalyzer
8
+
9
+
10
+ @click.group()
11
+ @click.version_option(version="1.0.0")
12
+ def cli():
13
+ """Java Class Analyzer MCP Server CLI"""
14
+ pass
15
+
16
+
17
+ @cli.command()
18
+ def start():
19
+ """启动MCP服务器"""
20
+ try:
21
+ from .main import mcp
22
+
23
+ mcp.run(transport="stdio")
24
+ except Exception as e:
25
+ print(f"启动服务器失败: {e}", file=sys.stderr)
26
+ sys.exit(1)
27
+
28
+
29
+ @cli.command()
30
+ @click.option("-o", "--output", required=True, help="输出配置文件路径")
31
+ def config(output):
32
+ """生成MCP客户端配置模板"""
33
+ config_template = {
34
+ "mcpServers": {
35
+ "java-class-analyzer": {
36
+ "command": "java-class-analyzer-mcp",
37
+ "args": [],
38
+ "env": {
39
+ "LOG_LEVEL": "INFO",
40
+ "MAVEN_REPO": os.path.expanduser("~/.m2/repository"),
41
+ "JAVA_HOME": os.environ.get("JAVA_HOME", ""),
42
+ "CFR_PATH": "",
43
+ },
44
+ }
45
+ }
46
+ }
47
+
48
+ with open(output, "w", encoding="utf-8") as f:
49
+ json.dump(config_template, f, indent=2, ensure_ascii=False)
50
+
51
+ print(f"配置文件已生成: {output}")
52
+
53
+
54
+ @cli.command()
55
+ @click.option(
56
+ "-t",
57
+ "--tool",
58
+ type=click.Choice(["scan", "decompile", "analyze", "all"]),
59
+ required=True,
60
+ help="要测试的工具",
61
+ )
62
+ @click.option("-p", "--project", required=True, help="项目路径")
63
+ @click.option("-c", "--class-name", help="要分析的类名")
64
+ @click.option("--no-refresh", is_flag=True, help="不强制刷新依赖索引")
65
+ @click.option("--no-cache", is_flag=True, help="不使用反编译缓存")
66
+ def test(tool, project, class_name, no_refresh, no_cache):
67
+ """测试工具使用"""
68
+ project_path = os.path.abspath(project)
69
+
70
+ if not os.path.exists(project_path):
71
+ print(f"项目路径不存在: {project_path}")
72
+ sys.exit(1)
73
+
74
+ # 初始化服务
75
+ scanner = DependencyScanner()
76
+ decompiler = DecompilerService()
77
+ analyzer = JavaClassAnalyzer()
78
+
79
+ try:
80
+ if tool == "scan" or tool == "all":
81
+ print("=== 测试依赖扫描 ===")
82
+ result = scanner.scan_project(project_path, not no_refresh)
83
+ print(
84
+ f"扫描完成!处理了 {result.jar_count} 个JAR包,索引了 {result.class_count} 个类"
85
+ )
86
+ print(f"索引文件路径: {result.index_path}")
87
+ print("示例索引条目:")
88
+ for entry in result.sample_entries[:5]:
89
+ print(f" {entry}")
90
+ print()
91
+
92
+ if tool in ["decompile", "analyze", "all"]:
93
+ if not class_name:
94
+ print("请指定要分析的类名")
95
+ sys.exit(1)
96
+
97
+ # 确保索引存在
98
+ index_path = os.path.join(project_path, ".mcp-class-index.json")
99
+ if not os.path.exists(index_path):
100
+ print("正在创建类索引...")
101
+ scanner.scan_project(project_path, False)
102
+
103
+ if tool == "decompile" or tool == "all":
104
+ print("=== 测试类反编译 ===")
105
+ use_cache = not no_cache
106
+ source_code = decompiler.decompile_class(
107
+ class_name, project_path, use_cache
108
+ )
109
+ print(f"类 {class_name} 的反编译源码:")
110
+ print("=" * 50)
111
+ print(source_code)
112
+ print("=" * 50)
113
+ print()
114
+
115
+ if tool == "analyze" or tool == "all":
116
+ print("=== 测试类分析 ===")
117
+ analysis = analyzer.analyze_class(class_name, project_path)
118
+
119
+ print(f"类 {class_name} 的分析结果:")
120
+ print(f"包名: {analysis.package_name}")
121
+ print(f"类名: {analysis.class_name}")
122
+ print(f"修饰符: {' '.join(analysis.modifiers)}")
123
+ print(f"父类: {analysis.super_class or '无'}")
124
+ print(f"实现的接口: {', '.join(analysis.interfaces) or '无'}")
125
+
126
+ if analysis.fields:
127
+ print(f"\n字段 ({len(analysis.fields)}个):")
128
+ for field in analysis.fields:
129
+ print(f" - {' '.join(field.modifiers)} {field.type_} {field.name}")
130
+
131
+ if analysis.methods:
132
+ print(f"\n方法 ({len(analysis.methods)}个):")
133
+ for method in analysis.methods:
134
+ print(
135
+ f" - {' '.join(method.modifiers)} {method.return_type} {method.name}({', '.join(method.parameters)})"
136
+ )
137
+ print()
138
+
139
+ except Exception as e:
140
+ print(f"测试失败: {e}")
141
+ sys.exit(1)
142
+
143
+
144
+ if __name__ == "__main__":
145
+ cli()
@@ -0,0 +1,3 @@
1
+ """
2
+ Java类反编译模块
3
+ """