@yottameta/yotta-dev-mcp-plugin 0.2.0 → 0.2.2
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.
- package/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/yotta-dev-mcp/SKILL.md +18 -5
- package/skills/yotta-dev-mcp/references/tools.md +35 -2
- package/skills/yotta-dev-mcp/scripts/dev_common.py +13 -2
- package/skills/yotta-dev-mcp/scripts/dev_engine.py +72 -78
- package/skills/yotta-dev-mcp/scripts/dev_mcp_doctor.py +768 -0
- package/skills/yotta-dev-mcp/scripts/dev_model.py +20 -8
- package/skills/yotta-dev-mcp/scripts/dev_selftest.py +18 -7
- package/skills/yotta-dev-mcp/scripts/yotta_dev_mcp.py +98 -8
- package/skills/yotta-dev-mcp/server.json +2 -2
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"authentication": "ON_INSTALL"
|
|
13
13
|
},
|
|
14
14
|
"category": "Developer Tools",
|
|
15
|
-
"version": "0.2.
|
|
15
|
+
"version": "0.2.2",
|
|
16
16
|
"description": "YuanKai (yotta-dev-mcp) - deterministic local development tools exposed over stdio MCP: repo_map, system_model, architecture_review, impact_analysis, verify_change, self_test, run_adapter, find_code, compress_output, review_code, review_diff, mcp_doctor, scan_secrets, scan_dependencies, check_publish_readiness, run_checks, scaffold_skill and workflow_state. Reads an optional .yotta/architecture.json contract plus .yotta/verification.json policy for architecture reviews, change impact cones and the L0-L5 evidence ledger. Optional explicit adapters integrate import-linter, dependency-cruiser and Repomix without auto-install or network access. Python 3.8+ standard library, offline by default; writes and command execution require explicit flags.",
|
|
17
17
|
"interface": {
|
|
18
18
|
"displayName": "元开 yotta-dev-mcp",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"authentication": "ON_INSTALL"
|
|
13
13
|
},
|
|
14
14
|
"category": "Developer Tools",
|
|
15
|
-
"version": "0.2.
|
|
15
|
+
"version": "0.2.2",
|
|
16
16
|
"description": "YuanKai (yotta-dev-mcp) - deterministic local development tools exposed over stdio MCP: repo_map, system_model, architecture_review, impact_analysis, verify_change, self_test, run_adapter, find_code, compress_output, review_code, review_diff, mcp_doctor, scan_secrets, scan_dependencies, check_publish_readiness, run_checks, scaffold_skill and workflow_state. Reads an optional .yotta/architecture.json contract plus .yotta/verification.json policy for architecture reviews, change impact cones and the L0-L5 evidence ledger. Optional explicit adapters integrate import-linter, dependency-cruiser and Repomix without auto-install or network access. Python 3.8+ standard library, offline by default; writes and command execution require explicit flags.",
|
|
17
17
|
"interface": {
|
|
18
18
|
"displayName": "元开 yotta-dev-mcp",
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "yotta-dev-mcp",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"description": "YuanKai (yotta-dev-mcp) - deterministic local development tools exposed over stdio MCP: repo_map, system_model, architecture_review, impact_analysis, verify_change, self_test, run_adapter, find_code, compress_output, review_code, review_diff, mcp_doctor, scan_secrets, scan_dependencies, check_publish_readiness, run_checks, scaffold_skill and workflow_state. Reads an optional .yotta/architecture.json contract plus .yotta/verification.json policy for architecture reviews, change impact cones and the L0-L5 evidence ledger. Optional explicit adapters integrate import-linter, dependency-cruiser and Repomix without auto-install or network access. Python 3.8+ standard library, offline by default; writes and command execution require explicit flags.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "YottaMeta",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yotta-dev-mcp
|
|
3
3
|
description: 元开(yotta-dev-mcp)—— 本地、确定性的开发工具 MCP,把只读默认的开发能力暴露为 stdio MCP server:repo_map / system_model / architecture_review / impact_analysis / verify_change / self_test / run_adapter / find_code / compress_output / review_code / review_diff / mcp_doctor / scan_secrets / scan_dependencies / check_publish_readiness / run_checks / scaffold_skill / workflow_state。触发:让 AI 在陌生项目里先做结构盘点、构建系统模型或架构契约(.yotta/architecture.json)、按契约评审架构、分析改动影响锥与回归面、按验证阶梯产出证据账本、对元开自身做完整性 / 反证自测、探测或显式运行 import-linter / dependency-cruiser / Repomix 可选适配器、定位代码、评审改动、扫描密钥/依赖、检查发布就绪、运行白名单检查、生成脚手架或读取 .workflow 状态时;或用户说 元开 / 开发能力 MCP / yotta-dev-mcp / 代码库地图 / 系统模型 / 架构契约 / 架构评审 / 影响分析 / 验证账本 / 反证自测 / 代码评审 MCP / 适配器 等。边界:Python 3.8+ 标准库、离线默认;除 run_checks(显式 allow_execute)、verify_change 的 L2-L4 策略检查(显式 allow_execute)与 self_test 的测试子集(显式 allow_execute)、run_adapter 的 action=run(显式 allow_execute)、scaffold_skill / workflow_state 的显式 apply 外均为只读;不上传源码、不自动修改、不提交、不联网查询包是否存在、不自动安装或下载适配器。
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
license: MIT
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -37,6 +37,18 @@ npx -y @yottameta/yotta-dev-mcp
|
|
|
37
37
|
python scripts/yotta_dev_mcp.py
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
工具分组(`--tools`,与元忆同口径):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python scripts/yotta_dev_mcp.py --tools core # 5 工具:repo_map / find_code / review_code / review_diff / verify_change
|
|
44
|
+
python scripts/yotta_dev_mcp.py --tools full # 18 工具(默认)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`core` 给「需要把工具表常驻在上下文里」的宿主用:schema 体积约为 `full` 的 29%
|
|
48
|
+
(5 工具 3,883 字符 vs 18 工具 13,230 字符,紧凑 JSON 口径),适合默认启用;
|
|
49
|
+
`full` 暴露全部工具,适合按需调用、不常驻的场景。`core` 模式下调用 full 专属工具会返回
|
|
50
|
+
明确提示(提示用 `--tools full` 重启)。
|
|
51
|
+
|
|
40
52
|
自动接入说明:AI 首次协助配置时,必须先展示目标配置文件、完整配置片段和影响,
|
|
41
53
|
并获得用户明确同意后再写入 `mcpServers`。用户拒绝时不要写文件,直接提供上面的启动命令
|
|
42
54
|
让用户手动接入。
|
|
@@ -45,7 +57,7 @@ python scripts/yotta_dev_mcp.py
|
|
|
45
57
|
|
|
46
58
|
| 工具 | 用途 | 写入 |
|
|
47
59
|
|---|---|---|
|
|
48
|
-
| `repo_map` |
|
|
60
|
+
| `repo_map` | 模块、导入、入口点地图;支持 `from . import X` / 别名 / 包,忽略临时与探针目录 | 否 |
|
|
49
61
|
| `system_model` | 系统模型:模块、依赖、入口、测试映射、配置与数据归属;附带契约分层,输出 PASS / FAIL / UNKNOWN | 否 |
|
|
50
62
|
| `architecture_review` | 按契约评审依赖规则、边界可见性与数据归属;critical / high 判 FAIL,medium / low 只告警 | 否 |
|
|
51
63
|
| `impact_analysis` | 变更影响锥:直接消费者、受影响层 / 边界 / 存储 / 不变量、映射测试、风险分级与回滚探针 | 否 |
|
|
@@ -54,10 +66,10 @@ python scripts/yotta_dev_mcp.py
|
|
|
54
66
|
| `run_adapter` | 探测或显式运行 import-linter / dependency-cruiser / Repomix;固定 argv、项目内 cwd、有界输出与哈希 | 仅 action=run + 显式 allow_execute |
|
|
55
67
|
| `find_code` | 符号 / 文本定位,结果有上限 | 否 |
|
|
56
68
|
| `compress_output` | 保留错误与首尾的长输出压缩 | 否 |
|
|
57
|
-
| `review_code` |
|
|
69
|
+
| `review_code` | 规则化代码评审,带行号与建议;忽略 `.workflow` / `scratch` / `_probe` 等临时目录 | 否 |
|
|
58
70
|
| `review_diff` | 只评审 diff 的新增行 | 否 |
|
|
59
|
-
| `mcp_doctor` |
|
|
60
|
-
| `scan_secrets` | 密钥 / 凭据 /
|
|
71
|
+
| `mcp_doctor` | 技能版本与多宿主 MCP 配置体检;返回 coverage,只列 server 名,不含 command / env | 否 |
|
|
72
|
+
| `scan_secrets` | 密钥 / 凭据 / 高熵令牌扫描(强制脱敏;路径 / 哈希 / 文件名噪声过滤) | 否 |
|
|
61
73
|
| `scan_dependencies` | 依赖清单、lockfile、来源与 typosquat 启发式检查 | 否 |
|
|
62
74
|
| `check_publish_readiness` | 版本四件、发布文件、仓库与 publishConfig 检查 | 否 |
|
|
63
75
|
| `run_checks` | 白名单测试 / lint / compile 并返回结构化摘要 | 仅显式 allow_execute |
|
|
@@ -77,5 +89,6 @@ python scripts/yotta_dev_mcp.py
|
|
|
77
89
|
|
|
78
90
|
## 当前版本
|
|
79
91
|
|
|
92
|
+
- v0.2.1(2026-09-25):缺陷修复批次——`mcp_doctor` 改为多宿主 / 多根 / JSON+JSONC+TOML 子集发现并返回 coverage;`self_test(mode="installed")` 按精简分发副本 / npm / plugin / r0 形态判断 banner;`repo_map` 修复 `from . import X` 解析;`scan_secrets` 过滤路径 / 哈希 / 文件名噪声;`review_code` / `repo_map` 忽略 `.workflow` / `scratch` / `_probe` 等临时目录。
|
|
80
93
|
- v0.2.0(2026-09-25):新增 `system_model`、`architecture_review`、`impact_analysis`、`verify_change`、`self_test`、`run_adapter`,以及 `.yotta/architecture.json` 与可选 `.yotta/verification.json` 契约(版本 1);工具总数 18,原 12 个工具行为不变。
|
|
81
94
|
- v0.1.1:品牌显示名统一为「元开」;功能与 12 个工具不变。
|
|
@@ -14,6 +14,12 @@ Input:
|
|
|
14
14
|
|
|
15
15
|
Output: `root`, `modules`, `imports`, `entrypoints`, `truncated`.
|
|
16
16
|
|
|
17
|
+
Python imports include absolute, `from .X import Y`, `from . import X`,
|
|
18
|
+
`as` aliases, packages and multi-name forms. Temporary / agent-state
|
|
19
|
+
directories (`.workflow`, `.codex`, `.cursor`, `.claude`, `.agents`,
|
|
20
|
+
`scratch`, `_probe`, `probe`, `sandbox`, `debug`, `.tmp`) and probe/temp
|
|
21
|
+
file names are ignored by default.
|
|
22
|
+
|
|
17
23
|
## system_model
|
|
18
24
|
|
|
19
25
|
Input:
|
|
@@ -201,14 +207,36 @@ Input:
|
|
|
201
207
|
|
|
202
208
|
Only added lines are reviewed. Output: `files`, `findings`, `truncated`.
|
|
203
209
|
|
|
210
|
+
`review_code` uses the same default ignore set as `repo_map`: agent state
|
|
211
|
+
directories, scratch / probe / sandbox / debug directories and probe/temp
|
|
212
|
+
file names do not flood the result. `review_diff` only reviews added lines,
|
|
213
|
+
so it is unaffected by directory traversal.
|
|
214
|
+
|
|
204
215
|
## mcp_doctor
|
|
205
216
|
|
|
206
217
|
Input:
|
|
207
218
|
|
|
208
219
|
- `skills_dirs` (optional array).
|
|
209
220
|
- `config_paths` (optional array).
|
|
210
|
-
|
|
211
|
-
|
|
221
|
+
- `include_defaults` (optional boolean): when `config_paths` is supplied, also
|
|
222
|
+
scan the built-in host registry instead of explicit-only scope.
|
|
223
|
+
|
|
224
|
+
Output: `skills`, `mcp_configs`, `coverage`, `skills_coverage`, `issues`,
|
|
225
|
+
`coverage_gaps`, `unknown_hosts`, `summary`, `scope`, `checked_skills`,
|
|
226
|
+
`checked_configs`, `checked_hosts`.
|
|
227
|
+
|
|
228
|
+
Discovery is tiered and environment-aware: verified hosts include Codex
|
|
229
|
+
(`$CODEX_HOME/config.toml`, JSON fallbacks), Cursor, WorkBuddy
|
|
230
|
+
(`~/.workbuddy/mcp.json` plus `connectors/*/mcp.json`), OpenCode
|
|
231
|
+
(`$XDG_CONFIG_HOME/opencode/opencode.jsonc|json`), Claude Code, Windsurf,
|
|
232
|
+
Continue, Gemini, Qwen, Trae, Comate, CodeBuddy, Kimi, Kiro, VS Code and Zed;
|
|
233
|
+
additional hosts are best-effort candidates and are reported as
|
|
234
|
+
`unverified` when absent. JSON, JSONC and a narrow TOML `[mcp_servers.*]`
|
|
235
|
+
subset are parsed; YAML is reported as `unsupported`, never silently skipped.
|
|
236
|
+
Only server names are returned; commands, args and env values are never
|
|
237
|
+
included. `summary.all_clear` is true only for a fully covered, issue-free
|
|
238
|
+
default scan; always read `coverage_confidence` before treating it as
|
|
239
|
+
all-clear.
|
|
212
240
|
|
|
213
241
|
## scan_secrets
|
|
214
242
|
|
|
@@ -218,6 +246,11 @@ Input: `path` or `text`, optional `max_findings`, optional `include_git_history`
|
|
|
218
246
|
Output: `findings` with `path`, `line`, `rule`, `severity`, redacted `evidence`,
|
|
219
247
|
`suggestion`; `truncated`.
|
|
220
248
|
|
|
249
|
+
High-entropy findings apply a narrow noise filter for absolute paths,
|
|
250
|
+
URL / `file://` percent-encoded paths, common binary/source/document
|
|
251
|
+
suffixes and hash-context hex values (SHA-1/256/512, MD5, checksum, digest,
|
|
252
|
+
integrity). Credential-name, AWS-key and private-key rules are not relaxed.
|
|
253
|
+
|
|
221
254
|
## scan_dependencies
|
|
222
255
|
|
|
223
256
|
Input: `path`.
|
|
@@ -10,7 +10,14 @@ from pathlib import Path
|
|
|
10
10
|
IGNORE_DIRS = {
|
|
11
11
|
".git", ".hg", ".svn", "node_modules", "__pycache__", ".venv", "venv",
|
|
12
12
|
"dist", "build", ".next", ".nuxt", ".cache", ".tmp", ".tmp2",
|
|
13
|
+
".workflow", ".codex", ".cursor", ".claude", ".agents",
|
|
14
|
+
"scratch", "_scratch", "_probe", "probe", "probes",
|
|
15
|
+
"sandbox", ".sandbox", "debug", "_debug",
|
|
13
16
|
}
|
|
17
|
+
IGNORE_TEMP_FILE_RE = re.compile(
|
|
18
|
+
r"(?i)^(?:probe[_-].*|_probe[_-].*|scratch[_-].*|_scratch[_-].*|tmp_.*|temp_.*|.*_tmp)"
|
|
19
|
+
r"\.(?:py|js|jsx|mjs|cjs|ts|tsx|sh|ps1|go|rs|java|kt|kts|rb|php)$"
|
|
20
|
+
)
|
|
14
21
|
SOURCE_EXTS = {
|
|
15
22
|
".py", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".sh", ".ps1",
|
|
16
23
|
".go", ".rs", ".java", ".kt", ".kts", ".rb", ".php",
|
|
@@ -54,7 +61,9 @@ VERIFY_REQUIRED_SOURCE_FILES = (
|
|
|
54
61
|
"references/tools.md", "references/adapters.md",
|
|
55
62
|
"references/architecture-contract.md", "assets/banner.png",
|
|
56
63
|
)
|
|
57
|
-
VERIFY_REQUIRED_INSTALLED_FILES = ("SKILL.md",
|
|
64
|
+
VERIFY_REQUIRED_INSTALLED_FILES = ("SKILL.md",)
|
|
65
|
+
VERIFY_REQUIRED_INSTALLED_ASSETS = ("assets/banner.png",)
|
|
66
|
+
LEAN_INSTALL_MARKERS = ("_icon.png",)
|
|
58
67
|
VERIFY_WRITE_GATES = (
|
|
59
68
|
("run_checks", "allow_execute"),
|
|
60
69
|
("scaffold_skill", "apply"),
|
|
@@ -94,13 +103,15 @@ def _read_text(path):
|
|
|
94
103
|
return data.decode("utf-8", errors="replace")
|
|
95
104
|
|
|
96
105
|
|
|
97
|
-
def _iter_files(root, extensions=None, max_files=5000, all_files=False):
|
|
106
|
+
def _iter_files(root, extensions=None, max_files=5000, all_files=False, ignore_temp=True):
|
|
98
107
|
root = Path(root)
|
|
99
108
|
extensions = set(extensions or TEXT_EXTS)
|
|
100
109
|
count = 0
|
|
101
110
|
for current, dirs, files in os.walk(str(root)):
|
|
102
111
|
dirs[:] = sorted(d for d in dirs if d not in IGNORE_DIRS)
|
|
103
112
|
for name in sorted(files):
|
|
113
|
+
if ignore_temp and IGNORE_TEMP_FILE_RE.match(name):
|
|
114
|
+
continue
|
|
104
115
|
path = Path(current) / name
|
|
105
116
|
if path.is_symlink():
|
|
106
117
|
continue
|
|
@@ -26,12 +26,22 @@ from dev_common import (
|
|
|
26
26
|
_frontmatter_name, _frontmatter_version, source_exts,
|
|
27
27
|
)
|
|
28
28
|
from dev_impact import impact_analysis as _impact_analysis_impl
|
|
29
|
+
from dev_mcp_doctor import (
|
|
30
|
+
default_config_paths as _default_config_paths_impl,
|
|
31
|
+
default_skill_dirs as _default_skill_dirs_impl,
|
|
32
|
+
mcp_doctor as _mcp_doctor_impl,
|
|
33
|
+
)
|
|
29
34
|
from dev_verify import verify_change as _verify_change_impl
|
|
30
35
|
from dev_selftest import self_test
|
|
31
|
-
from dev_model import
|
|
36
|
+
from dev_model import (
|
|
37
|
+
_classify_python_import,
|
|
38
|
+
_repo_map_js,
|
|
39
|
+
_repo_map_python,
|
|
40
|
+
system_model as _system_model_impl,
|
|
41
|
+
)
|
|
32
42
|
from dev_rules import REVIEW_RULES
|
|
33
43
|
|
|
34
|
-
VERSION = "0.2.
|
|
44
|
+
VERSION = "0.2.2"
|
|
35
45
|
|
|
36
46
|
|
|
37
47
|
|
|
@@ -54,6 +64,7 @@ def repo_map(path, max_files=2000):
|
|
|
54
64
|
if len(files) > max_files:
|
|
55
65
|
truncated = True
|
|
56
66
|
files = files[:max_files]
|
|
67
|
+
module_set = {_rel(root, file_path) for file_path in files}
|
|
57
68
|
for file_path in files:
|
|
58
69
|
rel = _rel(root, file_path)
|
|
59
70
|
try:
|
|
@@ -63,7 +74,11 @@ def repo_map(path, max_files=2000):
|
|
|
63
74
|
language = _language(file_path)
|
|
64
75
|
modules.append({"path": rel, "language": language, "lines": len(text.splitlines())})
|
|
65
76
|
if file_path.suffix.lower() == ".py":
|
|
66
|
-
|
|
77
|
+
for raw in _repo_map_python(file_path, rel):
|
|
78
|
+
_, target = _classify_python_import(
|
|
79
|
+
raw["target"], rel, module_set, relative=raw.get("relative", False)
|
|
80
|
+
)
|
|
81
|
+
imports.append({"source": rel, "target": target, "line": raw["line"]})
|
|
67
82
|
elif file_path.suffix.lower() in (".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"):
|
|
68
83
|
imports.extend(_repo_map_js(file_path, rel))
|
|
69
84
|
if (
|
|
@@ -274,81 +289,19 @@ def review_diff(diff_text=None, path=None, base=None, max_findings=200):
|
|
|
274
289
|
|
|
275
290
|
|
|
276
291
|
def _default_skill_dirs():
|
|
277
|
-
|
|
278
|
-
candidates = [
|
|
279
|
-
home / ".codex" / "skills",
|
|
280
|
-
home / ".claude" / "skills",
|
|
281
|
-
home / ".cursor" / "skills",
|
|
282
|
-
home / ".config" / "opencode" / "skills",
|
|
283
|
-
]
|
|
284
|
-
codex_home = os.environ.get("CODEX_HOME")
|
|
285
|
-
if codex_home:
|
|
286
|
-
candidates.insert(0, Path(codex_home) / "skills")
|
|
287
|
-
claude_home = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
288
|
-
if claude_home:
|
|
289
|
-
candidates.insert(0, Path(claude_home) / "skills")
|
|
290
|
-
xdg_home = os.environ.get("XDG_CONFIG_HOME")
|
|
291
|
-
if xdg_home:
|
|
292
|
-
candidates.insert(0, Path(xdg_home) / "opencode" / "skills")
|
|
293
|
-
return candidates
|
|
292
|
+
return _default_skill_dirs_impl()
|
|
294
293
|
|
|
295
294
|
|
|
296
295
|
def _default_config_paths():
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
def mcp_doctor(skills_dirs=None, config_paths=None):
|
|
308
|
-
skills = []
|
|
309
|
-
issues = []
|
|
310
|
-
for directory in (skills_dirs or _default_skill_dirs()):
|
|
311
|
-
root = Path(directory)
|
|
312
|
-
if not root.is_dir():
|
|
313
|
-
continue
|
|
314
|
-
for child in sorted(root.iterdir(), key=lambda item: item.name):
|
|
315
|
-
skill_file = child / "SKILL.md"
|
|
316
|
-
if not child.is_dir() or not skill_file.is_file():
|
|
317
|
-
continue
|
|
318
|
-
try:
|
|
319
|
-
text = _read_text(skill_file)
|
|
320
|
-
except (OSError, ValueError) as exc:
|
|
321
|
-
issues.append("%s: %s" % (skill_file, exc))
|
|
322
|
-
continue
|
|
323
|
-
skills.append({
|
|
324
|
-
"name": _frontmatter_name(text) or child.name,
|
|
325
|
-
"version": _frontmatter_version(text),
|
|
326
|
-
"path": str(skill_file),
|
|
327
|
-
})
|
|
328
|
-
mcp_configs = []
|
|
329
|
-
for config_path in (config_paths or _default_config_paths()):
|
|
330
|
-
path = Path(config_path)
|
|
331
|
-
if not path.is_file():
|
|
332
|
-
continue
|
|
333
|
-
try:
|
|
334
|
-
payload = json.loads(_read_text(path))
|
|
335
|
-
except Exception as exc: # noqa: BLE001
|
|
336
|
-
issues.append("%s: %s" % (path, exc))
|
|
337
|
-
continue
|
|
338
|
-
servers = payload.get("mcpServers") if isinstance(payload, dict) else None
|
|
339
|
-
mcp_configs.append({
|
|
340
|
-
"path": str(path),
|
|
341
|
-
"servers": sorted(servers.keys()) if isinstance(servers, dict) else [],
|
|
342
|
-
})
|
|
343
|
-
skills.sort(key=lambda item: (item["name"], item["path"]))
|
|
344
|
-
mcp_configs.sort(key=lambda item: item["path"])
|
|
345
|
-
return {
|
|
346
|
-
"skills": skills,
|
|
347
|
-
"mcp_configs": mcp_configs,
|
|
348
|
-
"issues": issues,
|
|
349
|
-
"checked_skills": len(skills),
|
|
350
|
-
"checked_configs": len(mcp_configs),
|
|
351
|
-
}
|
|
296
|
+
return _default_config_paths_impl()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def mcp_doctor(skills_dirs=None, config_paths=None, include_defaults=None):
|
|
300
|
+
return _mcp_doctor_impl(
|
|
301
|
+
skills_dirs=skills_dirs,
|
|
302
|
+
config_paths=config_paths,
|
|
303
|
+
include_defaults=include_defaults,
|
|
304
|
+
)
|
|
352
305
|
|
|
353
306
|
|
|
354
307
|
SECRET_KEY_RE = re.compile(
|
|
@@ -357,6 +310,35 @@ SECRET_KEY_RE = re.compile(
|
|
|
357
310
|
AWS_KEY_RE = re.compile(r"\bAKIA[0-9A-Z]{16}\b")
|
|
358
311
|
PRIVATE_KEY_RE = re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")
|
|
359
312
|
HIGH_ENTROPY_RE = re.compile(r"[A-Za-z0-9_+/=\-]{32,}")
|
|
313
|
+
HASH_CONTEXT_RE = re.compile(r"(?i)(sha1|sha256|sha512|hash|checksum|digest|integrity)")
|
|
314
|
+
HEX_TOKEN_RE = re.compile(r"^[0-9a-fA-F]+$")
|
|
315
|
+
PATH_HINT_RE = re.compile(r"(?i)(?:[a-z]:[\\/]|\\\\|https?://|file://)")
|
|
316
|
+
URL_HINT_RE = re.compile(r"(?i)(?:file|https?)://")
|
|
317
|
+
PERCENT_ESCAPE_RE = re.compile(r"%[0-9A-Fa-f]{2}")
|
|
318
|
+
HASH_PREFIX_RE = re.compile(r"(?i)^(?:sha1|sha256|sha512|md5)[=:]([0-9a-fA-F]{32,128})$")
|
|
319
|
+
FILE_SUFFIX_RE = re.compile(
|
|
320
|
+
r"(?i)\.(exe|dll|sys|py|js|ts|tsx|json|md|txt|log|whl|tar|gz|zip|png|jpg|jpeg|svg)"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _high_entropy_noise(line, token, start, end):
|
|
325
|
+
"""Return True for common non-secret high-entropy noise."""
|
|
326
|
+
if HASH_PREFIX_RE.match(token):
|
|
327
|
+
return True
|
|
328
|
+
if PERCENT_ESCAPE_RE.search(token) and URL_HINT_RE.search(line):
|
|
329
|
+
return True
|
|
330
|
+
if start > 0 and line[start - 1] == "%":
|
|
331
|
+
return True
|
|
332
|
+
if HEX_TOKEN_RE.match(token) and len(token) in (32, 40, 64, 128) and HASH_CONTEXT_RE.search(line):
|
|
333
|
+
return True
|
|
334
|
+
window = line[max(0, start - 16):min(len(line), end + 16)]
|
|
335
|
+
if PATH_HINT_RE.search(window):
|
|
336
|
+
return True
|
|
337
|
+
if URL_HINT_RE.search(line) and PERCENT_ESCAPE_RE.search(window):
|
|
338
|
+
return True
|
|
339
|
+
if FILE_SUFFIX_RE.match(line[end:end + 8]):
|
|
340
|
+
return True
|
|
341
|
+
return False
|
|
360
342
|
|
|
361
343
|
|
|
362
344
|
def _entropy(value):
|
|
@@ -385,7 +367,9 @@ def scan_secrets(path=None, text=None, max_findings=200, include_git_history=Fal
|
|
|
385
367
|
root = Path(path)
|
|
386
368
|
if not root.exists():
|
|
387
369
|
raise ValueError("路径不存在: %s" % path)
|
|
388
|
-
files = [root] if root.is_file() else list(
|
|
370
|
+
files = [root] if root.is_file() else list(
|
|
371
|
+
_iter_files(root, all_files=True, ignore_temp=False)
|
|
372
|
+
)
|
|
389
373
|
base = root.parent if root.is_file() else root
|
|
390
374
|
for file_path in files:
|
|
391
375
|
try:
|
|
@@ -421,7 +405,11 @@ def scan_secrets(path=None, text=None, max_findings=200, include_git_history=Fal
|
|
|
421
405
|
})
|
|
422
406
|
for match in HIGH_ENTROPY_RE.finditer(line):
|
|
423
407
|
token = match.group(0)
|
|
424
|
-
if
|
|
408
|
+
if (
|
|
409
|
+
_entropy(token) >= 4.0
|
|
410
|
+
and not PRIVATE_KEY_RE.search(line)
|
|
411
|
+
and not _high_entropy_noise(line, token, match.start(), match.end())
|
|
412
|
+
):
|
|
425
413
|
findings.append({
|
|
426
414
|
"path": rel, "line": line_no, "rule": "high-entropy-token",
|
|
427
415
|
"severity": "medium", "evidence": _redact(token),
|
|
@@ -1083,6 +1071,8 @@ def main():
|
|
|
1083
1071
|
doctor = sub.add_parser("mcp-doctor")
|
|
1084
1072
|
doctor.add_argument("--skills-dir", action="append")
|
|
1085
1073
|
doctor.add_argument("--config", action="append")
|
|
1074
|
+
doctor.add_argument("--include-defaults", action="store_true",
|
|
1075
|
+
help="scan built-in host registry in addition to --config paths")
|
|
1086
1076
|
args = parser.parse_args()
|
|
1087
1077
|
if args.command == "repo-map":
|
|
1088
1078
|
result = repo_map(args.path)
|
|
@@ -1126,7 +1116,11 @@ def main():
|
|
|
1126
1116
|
elif args.command == "review-code":
|
|
1127
1117
|
result = review_code(args.path)
|
|
1128
1118
|
elif args.command == "mcp-doctor":
|
|
1129
|
-
result = mcp_doctor(
|
|
1119
|
+
result = mcp_doctor(
|
|
1120
|
+
skills_dirs=args.skills_dir,
|
|
1121
|
+
config_paths=args.config,
|
|
1122
|
+
include_defaults=args.include_defaults,
|
|
1123
|
+
)
|
|
1130
1124
|
else:
|
|
1131
1125
|
parser.print_help()
|
|
1132
1126
|
return 2
|