diffsense 2.2.12__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.
Files changed (58) hide show
  1. adapters/__init__.py +0 -0
  2. adapters/base.py +27 -0
  3. adapters/github_adapter.py +164 -0
  4. adapters/gitlab_adapter.py +207 -0
  5. adapters/local_adapter.py +136 -0
  6. banner.py +71 -0
  7. cli.py +606 -0
  8. config/__init__.py +1 -0
  9. config/rules.yaml +371 -0
  10. core/__init__.py +235 -0
  11. core/ast_detector.py +853 -0
  12. core/change.py +46 -0
  13. core/composer.py +93 -0
  14. core/evaluator.py +15 -0
  15. core/ignore_manager.py +71 -0
  16. core/knowledge.py +77 -0
  17. core/parser.py +181 -0
  18. core/parser_manager.py +104 -0
  19. core/quality_manager.py +117 -0
  20. core/renderer.py +197 -0
  21. core/rule_base.py +98 -0
  22. core/rule_runtime.py +103 -0
  23. core/rules.py +718 -0
  24. core/run_config.py +85 -0
  25. core/semantic_diff.py +359 -0
  26. core/signal_model.py +21 -0
  27. core/signals_registry.py +62 -0
  28. diffsense-2.2.12.dist-info/METADATA +18 -0
  29. diffsense-2.2.12.dist-info/RECORD +58 -0
  30. diffsense-2.2.12.dist-info/WHEEL +5 -0
  31. diffsense-2.2.12.dist-info/entry_points.txt +3 -0
  32. diffsense-2.2.12.dist-info/licenses/LICENSE +176 -0
  33. diffsense-2.2.12.dist-info/top_level.txt +11 -0
  34. diffsense_mcp/__init__.py +1 -0
  35. diffsense_mcp/launcher.py +28 -0
  36. diffsense_mcp/server.py +687 -0
  37. governance/lifecycle.py +54 -0
  38. main.py +318 -0
  39. rules/__init__.py +246 -0
  40. rules/api_compatibility.py +372 -0
  41. rules/collection_handling.py +349 -0
  42. rules/concurrency.py +194 -0
  43. rules/concurrency_adapter.py +250 -0
  44. rules/cross_language_adapter.py +444 -0
  45. rules/exception_handling.py +320 -0
  46. rules/go_rules.py +401 -0
  47. rules/null_safety.py +301 -0
  48. rules/resource_management.py +222 -0
  49. rules/yaml_adapter.py +195 -0
  50. run_audit.py +478 -0
  51. sdk/cpp_adapter.py +238 -0
  52. sdk/go_adapter.py +199 -0
  53. sdk/java_adapter.py +199 -0
  54. sdk/javascript_adapter.py +229 -0
  55. sdk/language_adapter.py +313 -0
  56. sdk/python_adapter.py +195 -0
  57. sdk/rule.py +63 -0
  58. sdk/signal.py +14 -0
@@ -0,0 +1,687 @@
1
+ """
2
+ DiffSense MCP Server
3
+
4
+ 使用 FastMCP 框架实现的 MCP Server,提供代码审计工具供 AI Agent 调用。
5
+ 支持多语言规则自动适配和 Git 联动。
6
+ """
7
+ import os
8
+ import json
9
+ import subprocess
10
+ import fnmatch
11
+ from typing import Optional, Dict, Any, List
12
+ from mcp.server.fastmcp import FastMCP
13
+
14
+ # 创建 FastMCP 实例
15
+ mcp = FastMCP("DiffSense", json_response=True)
16
+
17
+ # 默认配置
18
+ DEFAULT_RULES_PATH = "config"
19
+ DEFAULT_OUTPUT_DIR = "diffsense-mcp-output"
20
+
21
+ # 语言与文件扩展名映射
22
+ LANG_EXTENSIONS = {
23
+ "python": [".py"],
24
+ "javascript": [".js", ".jsx", ".ts", ".tsx", ".mjs"],
25
+ "java": [".java"],
26
+ "go": [".go"],
27
+ "cpp": [".cpp", ".cxx", ".cc", ".c", ".h", ".hpp", ".hxx"],
28
+ }
29
+
30
+ # 扩展名反查语言
31
+ EXT_TO_LANG = {}
32
+ for lang, exts in LANG_EXTENSIONS.items():
33
+ for ext in exts:
34
+ EXT_TO_LANG[ext] = lang
35
+
36
+
37
+ def _detect_languages_from_diff(diff_content: str) -> List[str]:
38
+ """从 diff 内容中检测涉及的语言"""
39
+ languages = set()
40
+ for line in diff_content.splitlines():
41
+ if line.startswith("+++ b/") or line.startswith("--- a/"):
42
+ path = line.split("/", 1)[-1] if "/" in line else line[6:]
43
+ _, ext = os.path.splitext(path)
44
+ if ext in EXT_TO_LANG:
45
+ languages.add(EXT_TO_LANG[ext])
46
+ return sorted(languages)
47
+
48
+
49
+ def _detect_languages_from_files(file_paths: List[str]) -> List[str]:
50
+ """从文件列表中检测涉及的语言"""
51
+ languages = set()
52
+ for path in file_paths:
53
+ _, ext = os.path.splitext(path)
54
+ if ext in EXT_TO_LANG:
55
+ languages.add(EXT_TO_LANG[ext])
56
+ return sorted(languages)
57
+
58
+
59
+ def _get_rules_path_for_languages(base_rules_path: str, languages: List[str]) -> str:
60
+ """根据检测到的语言返回合适的规则路径"""
61
+ return base_rules_path
62
+
63
+
64
+ def _run_git(args: List[str], cwd: str = ".") -> tuple:
65
+ """执行 git 命令并返回 (returncode, stdout, stderr)"""
66
+ try:
67
+ result = subprocess.run(
68
+ ["git"] + args,
69
+ capture_output=True,
70
+ text=True,
71
+ cwd=cwd,
72
+ timeout=30,
73
+ encoding="utf-8",
74
+ errors="replace",
75
+ )
76
+ return result.returncode, result.stdout, result.stderr
77
+ except FileNotFoundError:
78
+ return 1, "", "git not found"
79
+ except subprocess.TimeoutExpired:
80
+ return 1, "", "git command timed out"
81
+ except Exception as e:
82
+ return 1, "", str(e)
83
+
84
+
85
+ def _is_git_repo(path: str = ".") -> bool:
86
+ """检查是否是 git 仓库"""
87
+ code, _, _ = _run_git(["rev-parse", "--is-inside-work-tree"], cwd=path)
88
+ return code == 0
89
+
90
+
91
+ # ============================================================
92
+ # Tools
93
+ # ============================================================
94
+
95
+ @mcp.tool()
96
+ async def audit_diff(
97
+ diff_content: str,
98
+ rules_path: str = DEFAULT_RULES_PATH,
99
+ profile: Optional[str] = None,
100
+ output_dir: str = DEFAULT_OUTPUT_DIR,
101
+ ) -> Dict[str, Any]:
102
+ """
103
+ 审计代码变更 diff 内容。自动识别 diff 中涉及的语言并加载对应规则。
104
+
105
+ Args:
106
+ diff_content: Git unified diff 内容
107
+ rules_path: 规则配置文件或目录路径
108
+ profile: 规则 profile (strict, lightweight, 或 None)
109
+ output_dir: 输出目录路径
110
+
111
+ Returns:
112
+ 结构化审计结果,包含 review_level, details, _metrics, _languages 等字段
113
+ """
114
+ from diffsense.core import analyze_diff, build_inline_comments
115
+ from diffsense.adapters.local_adapter import LocalFileAdapter
116
+
117
+ # 检测涉及的语言
118
+ languages = _detect_languages_from_diff(diff_content)
119
+
120
+ os.makedirs(output_dir, exist_ok=True)
121
+
122
+ # 执行核心分析
123
+ result = analyze_diff(
124
+ diff_content=diff_content,
125
+ rules_path=rules_path,
126
+ profile=profile,
127
+ )
128
+
129
+ # 构建内联评论
130
+ from diffsense.core.parser import DiffParser
131
+ parser = DiffParser()
132
+ diff_data = parser.parse(diff_content)
133
+ inline_comments = build_inline_comments(result.get("details", []), diff_data)
134
+
135
+ # 保存结果
136
+ adapter = LocalFileAdapter(output_dir=output_dir)
137
+ adapter.save_report(result)
138
+ adapter.post_inline_comments(inline_comments)
139
+
140
+ # 生成 HTML 报告
141
+ try:
142
+ from diffsense.core.renderer import HtmlRenderer
143
+ html_renderer = HtmlRenderer()
144
+ html_report = html_renderer.render(result)
145
+ adapter.save_html_report(html_report)
146
+ except Exception:
147
+ pass
148
+
149
+ # 添加语言信息
150
+ result["_languages"] = languages
151
+ result["_output_paths"] = adapter.get_output_paths()
152
+
153
+ return result
154
+
155
+
156
+ @mcp.tool()
157
+ async def audit_diff_file(
158
+ diff_file_path: str,
159
+ rules_path: str = DEFAULT_RULES_PATH,
160
+ profile: Optional[str] = None,
161
+ output_dir: str = DEFAULT_OUTPUT_DIR,
162
+ ) -> Dict[str, Any]:
163
+ """
164
+ 审计本地 diff 文件。
165
+
166
+ Args:
167
+ diff_file_path: diff 文件路径
168
+ rules_path: 规则配置文件或目录路径
169
+ profile: 规则 profile (strict, lightweight, 或 None)
170
+ output_dir: 输出目录路径
171
+
172
+ Returns:
173
+ 结构化审计结果
174
+ """
175
+ from diffsense.adapters.local_adapter import LocalFileAdapter
176
+
177
+ adapter = LocalFileAdapter(diff_file_path=diff_file_path)
178
+ diff_content = adapter.fetch_diff()
179
+
180
+ return await audit_diff(
181
+ diff_content=diff_content,
182
+ rules_path=rules_path,
183
+ profile=profile,
184
+ output_dir=output_dir,
185
+ )
186
+
187
+
188
+ @mcp.tool()
189
+ async def audit_git_changes(
190
+ repo_path: str = ".",
191
+ base: str = "HEAD~1",
192
+ head: str = "HEAD",
193
+ rules_path: str = DEFAULT_RULES_PATH,
194
+ profile: Optional[str] = None,
195
+ output_dir: str = DEFAULT_OUTPUT_DIR,
196
+ ) -> Dict[str, Any]:
197
+ """
198
+ 审计 Git 仓库中两个提交之间的代码变更。与 Git 联动,自动获取 diff。
199
+
200
+ 适用于:
201
+ - 审计最近一次提交的变更
202
+ - 对比两个分支的差异
203
+ - 审计某个 PR 的所有变更
204
+
205
+ Args:
206
+ repo_path: Git 仓库路径
207
+ base: 基准提交(默认 HEAD~1,即最近一次提交)
208
+ head: 目标提交(默认 HEAD)
209
+ rules_path: 规则配置文件或目录路径
210
+ profile: 规则 profile (strict, lightweight, 或 None)
211
+ output_dir: 输出目录路径
212
+
213
+ Returns:
214
+ 结构化审计结果,额外包含 git_info 字段
215
+ """
216
+ # 检查是否是 git 仓库
217
+ if not _is_git_repo(repo_path):
218
+ return {
219
+ "review_level": "error",
220
+ "message": f"Not a git repository: {repo_path}",
221
+ "details": [],
222
+ }
223
+
224
+ # 获取 diff
225
+ code, stdout, stderr = _run_git(
226
+ ["diff", "--no-color", f"{base}...{head}"],
227
+ cwd=repo_path,
228
+ )
229
+
230
+ if code != 0:
231
+ # 尝试三点语法失败,用两点语法
232
+ code, stdout, stderr = _run_git(
233
+ ["diff", "--no-color", f"{base}", f"{head}"],
234
+ cwd=repo_path,
235
+ )
236
+
237
+ if code != 0:
238
+ return {
239
+ "review_level": "error",
240
+ "message": f"Failed to get git diff: {stderr}",
241
+ "details": [],
242
+ }
243
+
244
+ diff_content = stdout
245
+ if not diff_content.strip():
246
+ return {
247
+ "review_level": "pass",
248
+ "message": "No changes found between the specified commits.",
249
+ "details": [],
250
+ "git_info": {"base": base, "head": head, "has_changes": False},
251
+ }
252
+
253
+ # 获取提交信息
254
+ _, log_stdout, _ = _run_git(
255
+ ["log", "--oneline", f"{base}..{head}"],
256
+ cwd=repo_path,
257
+ )
258
+
259
+ # 获取变更文件列表
260
+ _, files_stdout, _ = _run_git(
261
+ ["diff", "--name-only", f"{base}", f"{head}"],
262
+ cwd=repo_path,
263
+ )
264
+
265
+ changed_files = [f for f in files_stdout.strip().splitlines() if f]
266
+
267
+ # 调用核心审计
268
+ result = await audit_diff(
269
+ diff_content=diff_content,
270
+ rules_path=rules_path,
271
+ profile=profile,
272
+ output_dir=output_dir,
273
+ )
274
+
275
+ # 添加 Git 信息
276
+ result["git_info"] = {
277
+ "base": base,
278
+ "head": head,
279
+ "has_changes": True,
280
+ "commits": log_stdout.strip().splitlines(),
281
+ "changed_files": changed_files,
282
+ }
283
+
284
+ return result
285
+
286
+
287
+ @mcp.tool()
288
+ async def audit_git_staged(
289
+ repo_path: str = ".",
290
+ rules_path: str = DEFAULT_RULES_PATH,
291
+ profile: Optional[str] = None,
292
+ output_dir: str = DEFAULT_OUTPUT_DIR,
293
+ ) -> Dict[str, Any]:
294
+ """
295
+ 审计 Git 暂存区(staged)的代码变更。在 commit 之前进行预检。
296
+
297
+ 适用于:
298
+ - 提交前自动审计
299
+ - 作为 pre-commit hook 的替代方案
300
+ - AI Agent 辅助代码审查
301
+
302
+ Args:
303
+ repo_path: Git 仓库路径
304
+ rules_path: 规则配置文件或目录路径
305
+ profile: 规则 profile (strict, lightweight, 或 None)
306
+ output_dir: 输出目录路径
307
+
308
+ Returns:
309
+ 结构化审计结果
310
+ """
311
+ if not _is_git_repo(repo_path):
312
+ return {
313
+ "review_level": "error",
314
+ "message": f"Not a git repository: {repo_path}",
315
+ "details": [],
316
+ }
317
+
318
+ # 获取暂存区 diff
319
+ code, stdout, stderr = _run_git(
320
+ ["diff", "--cached", "--no-color"],
321
+ cwd=repo_path,
322
+ )
323
+
324
+ if code != 0:
325
+ return {
326
+ "review_level": "error",
327
+ "message": f"Failed to get staged diff: {stderr}",
328
+ "details": [],
329
+ }
330
+
331
+ diff_content = stdout
332
+ if not diff_content.strip():
333
+ return {
334
+ "review_level": "pass",
335
+ "message": "No staged changes found.",
336
+ "details": [],
337
+ }
338
+
339
+ # 获取暂存文件列表
340
+ _, files_stdout, _ = _run_git(
341
+ ["diff", "--cached", "--name-only"],
342
+ cwd=repo_path,
343
+ )
344
+
345
+ staged_files = [f for f in files_stdout.strip().splitlines() if f]
346
+
347
+ # 调用核心审计
348
+ result = await audit_diff(
349
+ diff_content=diff_content,
350
+ rules_path=rules_path,
351
+ profile=profile,
352
+ output_dir=output_dir,
353
+ )
354
+
355
+ result["git_info"] = {
356
+ "type": "staged",
357
+ "staged_files": staged_files,
358
+ }
359
+
360
+ return result
361
+
362
+
363
+ @mcp.tool()
364
+ async def audit_workspace(
365
+ workspace_path: str = ".",
366
+ file_patterns: str = "*.py,*.js,*.ts,*.java,*.go,*.cpp",
367
+ rules_path: str = DEFAULT_RULES_PATH,
368
+ profile: Optional[str] = None,
369
+ output_dir: str = DEFAULT_OUTPUT_DIR,
370
+ ) -> Dict[str, Any]:
371
+ """
372
+ 审计整个工作区的代码文件。自动识别语言并加载对应规则。
373
+
374
+ 扫描指定目录下的代码文件,检测潜在的安全问题、性能问题、代码异味等。
375
+ 适用于对整个项目进行安全审计或代码质量评估。
376
+
377
+ Args:
378
+ workspace_path: 工作区根目录路径
379
+ file_patterns: 要扫描的文件模式(逗号分隔),如 "*.py,*.js"
380
+ rules_path: 规则配置文件或目录路径
381
+ profile: 规则 profile (strict, lightweight, 或 None)
382
+ output_dir: 输出目录路径
383
+
384
+ Returns:
385
+ 结构化审计结果
386
+ """
387
+ from diffsense.core import analyze_diff
388
+ from diffsense.adapters.local_adapter import LocalFileAdapter
389
+
390
+ patterns = [p.strip() for p in file_patterns.split(",")]
391
+
392
+ # 收集文件
393
+ scan_files = []
394
+ skip_dirs = {
395
+ '.git', '__pycache__', 'node_modules', 'venv', '.venv',
396
+ 'dist', 'build', '.idea', '.vscode', 'target', 'bin', 'obj',
397
+ '.tox', '.mypy_cache', '.pytest_cache', 'egg-info',
398
+ }
399
+
400
+ for root, dirs, files in os.walk(workspace_path):
401
+ dirs[:] = [d for d in dirs if d not in skip_dirs]
402
+ for filename in files:
403
+ for pattern in patterns:
404
+ if fnmatch.fnmatch(filename, pattern):
405
+ scan_files.append(os.path.join(root, filename))
406
+ break
407
+
408
+ if not scan_files:
409
+ return {
410
+ "review_level": "pass",
411
+ "message": f"No files found matching patterns: {file_patterns}",
412
+ "total_files_scanned": 0,
413
+ "total_issues": 0,
414
+ "issues_by_file": {},
415
+ "severity_summary": {},
416
+ "_languages": [],
417
+ }
418
+
419
+ # 检测语言
420
+ languages = _detect_languages_from_files(scan_files)
421
+
422
+ # 审计每个文件
423
+ all_issues = []
424
+ files_with_issues = 0
425
+
426
+ for file_path in scan_files:
427
+ try:
428
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
429
+ content = f.read()
430
+
431
+ if not content.strip():
432
+ continue
433
+
434
+ rel_path = os.path.relpath(file_path, workspace_path)
435
+ line_count = len(content.splitlines())
436
+
437
+ # 构造 diff(整个文件作为新增)
438
+ diff_content = f"diff --git a/{rel_path} b/{rel_path}\n"
439
+ diff_content += f"new file mode 100644\n"
440
+ diff_content += f"--- /dev/null\n"
441
+ diff_content += f"+++ b/{rel_path}\n"
442
+ diff_content += f"@@ -0,0 +1,{line_count} @@\n"
443
+
444
+ for line in content.splitlines():
445
+ diff_content += f"+{line}\n"
446
+
447
+ result = analyze_diff(
448
+ diff_content=diff_content,
449
+ rules_path=rules_path,
450
+ profile=profile,
451
+ )
452
+
453
+ issues = result.get("details", [])
454
+ if issues:
455
+ files_with_issues += 1
456
+ for issue in issues:
457
+ issue["file"] = rel_path
458
+ all_issues.append(issue)
459
+
460
+ except Exception:
461
+ continue
462
+
463
+ # 统计
464
+ severity_summary = {"critical": 0, "high": 0, "medium": 0, "low": 0, "unknown": 0}
465
+ for issue in all_issues:
466
+ sev = issue.get("severity", "unknown")
467
+ if sev in severity_summary:
468
+ severity_summary[sev] += 1
469
+
470
+ if severity_summary["critical"] > 0:
471
+ review_level = "critical"
472
+ elif severity_summary["high"] > 0:
473
+ review_level = "high"
474
+ elif severity_summary["medium"] > 0:
475
+ review_level = "medium"
476
+ elif severity_summary["low"] > 0:
477
+ review_level = "low"
478
+ else:
479
+ review_level = "pass"
480
+
481
+ issues_by_file = {}
482
+ for issue in all_issues:
483
+ fp = issue.get("file", "unknown")
484
+ if fp not in issues_by_file:
485
+ issues_by_file[fp] = []
486
+ issues_by_file[fp].append(issue)
487
+
488
+ # 保存结果
489
+ os.makedirs(output_dir, exist_ok=True)
490
+ output_data = {
491
+ "review_level": review_level,
492
+ "total_files_scanned": len(scan_files),
493
+ "total_issues": len(all_issues),
494
+ "files_with_issues": files_with_issues,
495
+ "severity_summary": severity_summary,
496
+ "issues_by_file": issues_by_file,
497
+ "_languages": languages,
498
+ }
499
+
500
+ adapter = LocalFileAdapter(output_dir=output_dir)
501
+ adapter.save_report(output_data)
502
+
503
+ return output_data
504
+
505
+
506
+ @mcp.tool()
507
+ async def get_audit_summary(
508
+ report_data: str,
509
+ ) -> str:
510
+ """
511
+ 获取审计报告的文本摘要(用于在 AI 对话中展示)。
512
+
513
+ Args:
514
+ report_data: audit_diff/audit_git_changes 等返回的 JSON 字符串
515
+
516
+ Returns:
517
+ 格式化的文本摘要
518
+ """
519
+ try:
520
+ data = json.loads(report_data) if isinstance(report_data, str) else report_data
521
+ except (json.JSONDecodeError, TypeError):
522
+ data = {}
523
+
524
+ review_level = data.get("review_level", "unknown")
525
+ details = data.get("details", [])
526
+ metrics = data.get("_metrics", {})
527
+ languages = data.get("_languages", [])
528
+ git_info = data.get("git_info", {})
529
+
530
+ lines = []
531
+ lines.append("=" * 50)
532
+ lines.append("DiffSense Audit Summary")
533
+ lines.append("=" * 50)
534
+ lines.append(f"Review Level: {review_level.upper()}")
535
+
536
+ if languages:
537
+ lines.append(f"Languages: {', '.join(languages)}")
538
+
539
+ if git_info:
540
+ if "base" in git_info:
541
+ lines.append(f"Git: {git_info['base']}..{git_info['head']}")
542
+ if git_info.get("commits"):
543
+ lines.append(f"Commits: {len(git_info['commits'])}")
544
+ if git_info.get("changed_files"):
545
+ lines.append(f"Changed Files: {len(git_info['changed_files'])}")
546
+ elif git_info.get("type") == "staged":
547
+ lines.append(f"Git: staged changes ({len(git_info.get('staged_files', []))} files)")
548
+
549
+ lines.append(f"Total Issues: {len(details)}")
550
+
551
+ if details:
552
+ severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "unknown": 0}
553
+ for d in details:
554
+ sev = d.get("severity", "unknown")
555
+ if sev in severity_counts:
556
+ severity_counts[sev] += 1
557
+
558
+ lines.append("")
559
+ lines.append("Severity Breakdown:")
560
+ for sev in ["critical", "high", "medium", "low"]:
561
+ if severity_counts[sev] > 0:
562
+ lines.append(f" * {sev.upper()}: {severity_counts[sev]}")
563
+
564
+ lines.append("")
565
+ lines.append("Top Issues:")
566
+ for i, d in enumerate(details[:5], 1):
567
+ lines.append(f" {i}. [{d.get('severity', 'unknown').upper()}] {d.get('id', 'N/A')}")
568
+ title = d.get('title', d.get('rationale', 'N/A'))
569
+ lines.append(f" {str(title)[:60]}")
570
+ if d.get("matched_file") or d.get("file"):
571
+ lines.append(f" File: {d.get('matched_file', d.get('file', 'N/A'))}")
572
+ else:
573
+ lines.append("")
574
+ lines.append("No issues found!")
575
+
576
+ perf = data.get("_performance", {})
577
+ if perf:
578
+ lines.append(f"")
579
+ lines.append(f"Cache Hit Rate: {perf.get('cache_hit_rate_pct', 0)}%")
580
+
581
+ lines.append("=" * 50)
582
+
583
+ return "\n".join(lines)
584
+
585
+
586
+ # ============================================================
587
+ # Resources
588
+ # ============================================================
589
+
590
+ @mcp.resource("diffsense://rules")
591
+ async def list_audit_rules() -> List[Dict[str, Any]]:
592
+ """列出当前加载的审计规则"""
593
+ from diffsense.core.rules import RuleEngine
594
+
595
+ try:
596
+ engine = RuleEngine(DEFAULT_RULES_PATH)
597
+ rules = engine.get_all_rules()
598
+
599
+ return [
600
+ {
601
+ "id": r.get("id", ""),
602
+ "title": r.get("title", ""),
603
+ "severity": r.get("severity", "unknown"),
604
+ "language": r.get("language", "unknown"),
605
+ "description": r.get("description", "")[:200],
606
+ }
607
+ for r in rules
608
+ ]
609
+ except Exception as e:
610
+ return [{"error": str(e)}]
611
+
612
+
613
+ @mcp.resource("diffsense://config")
614
+ async def get_audit_config() -> Dict[str, Any]:
615
+ """获取当前审计配置"""
616
+ try:
617
+ from diffsense.core.run_config import get_run_config
618
+ return get_run_config(os.getcwd())
619
+ except Exception:
620
+ return {}
621
+
622
+
623
+ @mcp.resource("diffsense://languages")
624
+ async def list_supported_languages() -> Dict[str, Any]:
625
+ """列出支持的语言和对应的文件扩展名"""
626
+ return {
627
+ "languages": LANG_EXTENSIONS,
628
+ "total": len(LANG_EXTENSIONS),
629
+ }
630
+
631
+
632
+ # ============================================================
633
+ # Prompts
634
+ # ============================================================
635
+
636
+ @mcp.prompt()
637
+ def review_code_changes(
638
+ diff_content: str = "$DIFF_CONTENT",
639
+ focus_areas: str = "security,performance,best_practices",
640
+ ) -> str:
641
+ """
642
+ 生成代码审查提示词,用于让 AI 分析 diff。
643
+ """
644
+ return f"""请审查以下代码变更,重点关注以下方面:{focus_areas}
645
+
646
+ ## Diff 内容
647
+ ```
648
+ {diff_content}
649
+ ```
650
+
651
+ 请提供:
652
+ 1. 发现的问题及其严重程度
653
+ 2. 修复建议
654
+ 3. 代码质量评估"""
655
+
656
+
657
+ @mcp.prompt()
658
+ def review_git_changes(
659
+ repo_path: str = ".",
660
+ base: str = "HEAD~1",
661
+ head: str = "HEAD",
662
+ ) -> str:
663
+ """
664
+ 生成 Git 变更审查提示词。
665
+ """
666
+ return f"""请审查 Git 仓库 {repo_path} 中从 {base} 到 {head} 的代码变更。
667
+
668
+ 请使用 audit_git_changes 工具获取变更详情,然后分析:
669
+ 1. 变更的整体风险等级
670
+ 2. 每个文件的安全问题
671
+ 3. 修复建议"""
672
+
673
+
674
+ def main():
675
+ """MCP Server 入口点"""
676
+ import sys
677
+
678
+ transport = "stdio"
679
+ if len(sys.argv) > 1:
680
+ if sys.argv[1] == "--http":
681
+ transport = "streamable-http"
682
+
683
+ mcp.run(transport=transport)
684
+
685
+
686
+ if __name__ == "__main__":
687
+ main()