harmony-build 0.2.0
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/README.md +265 -0
- package/dist/packages/arkanalyzer-mcp/src/index.d.ts +5 -0
- package/dist/packages/arkanalyzer-mcp/src/index.js +220 -0
- package/dist/packages/arkanalyzer-mcp/src/index.js.map +1 -0
- package/dist/packages/arkcheck-mcp/src/index.d.ts +5 -0
- package/dist/packages/arkcheck-mcp/src/index.js +160 -0
- package/dist/packages/arkcheck-mcp/src/index.js.map +1 -0
- package/dist/packages/arkcheck-mcp/src/rules.d.ts +15 -0
- package/dist/packages/arkcheck-mcp/src/rules.js +90 -0
- package/dist/packages/arkcheck-mcp/src/rules.js.map +1 -0
- package/dist/packages/dev-mcp/src/hvigor-bridge.d.ts +31 -0
- package/dist/packages/dev-mcp/src/hvigor-bridge.js +179 -0
- package/dist/packages/dev-mcp/src/hvigor-bridge.js.map +1 -0
- package/dist/packages/dev-mcp/src/index.d.ts +5 -0
- package/dist/packages/dev-mcp/src/index.js +256 -0
- package/dist/packages/dev-mcp/src/index.js.map +1 -0
- package/dist/packages/dev-mcp/src/log-stream.d.ts +56 -0
- package/dist/packages/dev-mcp/src/log-stream.js +147 -0
- package/dist/packages/dev-mcp/src/log-stream.js.map +1 -0
- package/dist/packages/harmony-knowledge-mcp/src/index.d.ts +5 -0
- package/dist/packages/harmony-knowledge-mcp/src/index.js +164 -0
- package/dist/packages/harmony-knowledge-mcp/src/index.js.map +1 -0
- package/dist/packages/hdc-mcp/src/hdc-runner.d.ts +32 -0
- package/dist/packages/hdc-mcp/src/hdc-runner.js +60 -0
- package/dist/packages/hdc-mcp/src/hdc-runner.js.map +1 -0
- package/dist/packages/hdc-mcp/src/index.d.ts +5 -0
- package/dist/packages/hdc-mcp/src/index.js +155 -0
- package/dist/packages/hdc-mcp/src/index.js.map +1 -0
- package/dist/packages/hvigor-mcp/src/index.d.ts +5 -0
- package/dist/packages/hvigor-mcp/src/index.js +300 -0
- package/dist/packages/hvigor-mcp/src/index.js.map +1 -0
- package/dist/packages/hvigor-mcp/src/log-parser.d.ts +28 -0
- package/dist/packages/hvigor-mcp/src/log-parser.js +65 -0
- package/dist/packages/hvigor-mcp/src/log-parser.js.map +1 -0
- package/dist/packages/mcp-common/src/entry.d.ts +1 -0
- package/dist/packages/mcp-common/src/entry.js +22 -0
- package/dist/packages/mcp-common/src/entry.js.map +1 -0
- package/dist/packages/mcp-common/src/errors.d.ts +13 -0
- package/dist/packages/mcp-common/src/errors.js +23 -0
- package/dist/packages/mcp-common/src/errors.js.map +1 -0
- package/dist/packages/mcp-common/src/index.d.ts +6 -0
- package/dist/packages/mcp-common/src/index.js +7 -0
- package/dist/packages/mcp-common/src/index.js.map +1 -0
- package/dist/packages/mcp-common/src/logger.d.ts +11 -0
- package/dist/packages/mcp-common/src/logger.js +32 -0
- package/dist/packages/mcp-common/src/logger.js.map +1 -0
- package/dist/packages/mcp-common/src/mcp-helpers.d.ts +41 -0
- package/dist/packages/mcp-common/src/mcp-helpers.js +94 -0
- package/dist/packages/mcp-common/src/mcp-helpers.js.map +1 -0
- package/dist/packages/mcp-common/src/safe-exec.d.ts +23 -0
- package/dist/packages/mcp-common/src/safe-exec.js +74 -0
- package/dist/packages/mcp-common/src/safe-exec.js.map +1 -0
- package/dist/packages/mcp-common/src/sandbox.d.ts +31 -0
- package/dist/packages/mcp-common/src/sandbox.js +96 -0
- package/dist/packages/mcp-common/src/sandbox.js.map +1 -0
- package/dist/packages/profiler-mcp/src/index.d.ts +5 -0
- package/dist/packages/profiler-mcp/src/index.js +148 -0
- package/dist/packages/profiler-mcp/src/index.js.map +1 -0
- package/dist/packages/profiler-mcp/src/trace-analyzer.d.ts +38 -0
- package/dist/packages/profiler-mcp/src/trace-analyzer.js +95 -0
- package/dist/packages/profiler-mcp/src/trace-analyzer.js.map +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +61 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/server/handlers.d.ts +11 -0
- package/dist/src/server/handlers.js +33 -0
- package/dist/src/server/handlers.js.map +1 -0
- package/dist/src/server/tools.d.ts +23 -0
- package/dist/src/server/tools.js +57 -0
- package/dist/src/server/tools.js.map +1 -0
- package/package.json +54 -0
- package//345/217/221/345/270/203/346/214/207/345/215/227.md +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# HarmonyOS Toolchain MCP(鸿蒙工具链 MCP 套件 / harmony-build)
|
|
2
|
+
|
|
3
|
+
> 让 AI Agent(Claude Code / Cursor / Codex / 自研)**真正能操作鸿蒙工程**。
|
|
4
|
+
> 这是 [`提案四-鸿蒙工具链MCP-Server化.md`](../../proposals/提案四-鸿蒙工具链MCP-Server化.md) 的代码实现。
|
|
5
|
+
|
|
6
|
+
## Changelog
|
|
7
|
+
|
|
8
|
+
### v0.2.0 (2026-05-21)
|
|
9
|
+
|
|
10
|
+
- ✅ 新增聚合入口 `harmony-build` MCP Server:一进程、一个 stdio 连接,聚合 7 个子能力包共 **31 个工具**
|
|
11
|
+
- ✅ 工具名加 `<namespace>_` 前缀,避免不同子包重名(如 `hdc_list_devices` / `hvigor_build_hap` / `dev_run_app`)
|
|
12
|
+
- ✅ 提供 `npx harmony-build` 一行启动;支持配置到 Claude Code / Cursor / JoyCode 的 mcpServers 中
|
|
13
|
+
- ✅ 发布到 npm 官方源,包名 `harmony-build`
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 快速接入(v0.2.0)
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"harmony-build": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "harmony-build"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 包总览
|
|
33
|
+
|
|
34
|
+
| 包 | 作用 | 主要 Tool/Resource |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| **`@harmony-mcp/all`** ⭐ | **聚合包:一进程提供下面所有能力(推广首选)** | 自动给每个工具加 `<ns>_` 前缀,共 31 个工具 |
|
|
37
|
+
| `@harmony-mcp/hdc` | 设备 / 安装 / 截屏 / 日志 | `list_devices`、`install_hap`、`uninstall`、`start_ability`、`stop_ability`、`hilog_capture`、`screencap` |
|
|
38
|
+
| `@harmony-mcp/hvigor` | 构建 / 清理 / 签名 / 日志解析 | `build_hap`、`clean`、`sign_hap`、`parse_build_log`、`run_task` |
|
|
39
|
+
| `@harmony-mcp/profiler` | 性能 trace 抓取 + 分析(最高价值) | `capture_trace`、`analyze_trace`、`capture_and_analyze`、`cold_start_breakdown` |
|
|
40
|
+
| `@harmony-mcp/arkanalyzer` | ArkTS 静态分析 / 影响面 | `find_references`、`impact_analysis`、`list_components`、`count_state_vars` |
|
|
41
|
+
| `@harmony-mcp/arkcheck` | 高性能编码规则(内置 10 条) | `lint_file`、`lint_project`、`explain_rule`、`list_rules` |
|
|
42
|
+
| `@harmony-mcp/knowledge` | RAG 知识库(公司 SOP / 最佳实践) | `search_kb`、`read_doc` + `harmony-kb://` Resources |
|
|
43
|
+
| `@harmony-mcp/dev` | DevEco Run 等价编排 | `run_app`、`start_log_stream`、`read_log_stream`、`stop_log_stream`、`list_log_streams` |
|
|
44
|
+
| `@harmony-mcp/common` | 公共:安全 shell、沙箱、日志、MCP 脚手架 | — |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 30 秒开跑
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd scripts/mcp
|
|
52
|
+
npm install
|
|
53
|
+
npm run build
|
|
54
|
+
npm run demo # 跑端到端验证(不需要鸿蒙设备)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`demo` 会启动三个 MCP server 子进程,用真实 MCP 协议跑:
|
|
58
|
+
- `arkcheck` 扫一段故意写错的 ArkTS → 命中 4 条规则
|
|
59
|
+
- `arkanalyzer` 在本仓库跑组件统计
|
|
60
|
+
- `knowledge` 在 `docs/` + `proposals/` 里搜关键字
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 接入 JoyCode(一键脚本)
|
|
67
|
+
|
|
68
|
+
### 推荐:聚合模式(一个 server 装下所有)
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
cd scripts/mcp
|
|
72
|
+
npm run install:joycode # 默认 = 聚合模式,沙箱根 = 本仓库根目录
|
|
73
|
+
# 指定一个鸿蒙工程作为沙箱根:
|
|
74
|
+
npm run install:joycode -- --project-dir /ABS/PATH/TO/your/harmony/project
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
脚本会:
|
|
78
|
+
1. 自动备份 `~/.joycode/joycode-mcp.json` 到 `*.bak-<时间戳>`
|
|
79
|
+
2. 注册 **1 个** `harmony-mcp` server(聚合 7 个子能力,共 31 个工具)
|
|
80
|
+
3. 重启 JoyCode 后即可使用
|
|
81
|
+
|
|
82
|
+
聚合模式下,工具名带 namespace 前缀:`hdc_list_devices` / `hvigor_build_hap` / `dev_run_app` / `arkcheck_lint_project` ……
|
|
83
|
+
|
|
84
|
+
试一下:
|
|
85
|
+
> "用 hdc_list_devices 看看连了什么设备"
|
|
86
|
+
> "用 dev_run_app 把工程 /XX 跑到设备上,盯 10 秒日志"
|
|
87
|
+
|
|
88
|
+
### 多 server 模式(需要单独禁用某能力时)
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm run install:joycode -- --multi # 装 7 个独立 harmony-* server
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
适合:需要在 JoyCode UI 里单独关掉某个能力(如不让 AI 跑 hvigor);或要求强进程隔离(一个工具崩不影响其他)。
|
|
95
|
+
|
|
96
|
+
**两种模式不能并存**:要切换的话先手动从 `~/.joycode/joycode-mcp.json` 删掉旧的 `harmony-*` 条目再重装,或加 `--force`。
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 接入 Claude Code
|
|
101
|
+
|
|
102
|
+
把下面内容粘进 `~/.config/claude-code/mcp_servers.json`(或工程内 `.mcp.json`):
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"mcpServers": {
|
|
107
|
+
"harmony-hdc": {
|
|
108
|
+
"command": "node",
|
|
109
|
+
"args": ["/ABS/PATH/TO/scripts/mcp/packages/hdc-mcp/dist/index.js"]
|
|
110
|
+
},
|
|
111
|
+
"harmony-hvigor": {
|
|
112
|
+
"command": "node",
|
|
113
|
+
"args": ["/ABS/PATH/TO/scripts/mcp/packages/hvigor-mcp/dist/index.js"]
|
|
114
|
+
},
|
|
115
|
+
"harmony-profiler": {
|
|
116
|
+
"command": "node",
|
|
117
|
+
"args": ["/ABS/PATH/TO/scripts/mcp/packages/profiler-mcp/dist/index.js"]
|
|
118
|
+
},
|
|
119
|
+
"harmony-arkanalyzer": {
|
|
120
|
+
"command": "node",
|
|
121
|
+
"args": ["/ABS/PATH/TO/scripts/mcp/packages/arkanalyzer-mcp/dist/index.js"]
|
|
122
|
+
},
|
|
123
|
+
"harmony-arkcheck": {
|
|
124
|
+
"command": "node",
|
|
125
|
+
"args": ["/ABS/PATH/TO/scripts/mcp/packages/arkcheck-mcp/dist/index.js"]
|
|
126
|
+
},
|
|
127
|
+
"harmony-knowledge": {
|
|
128
|
+
"command": "node",
|
|
129
|
+
"args": ["/ABS/PATH/TO/scripts/mcp/packages/harmony-knowledge-mcp/dist/index.js"],
|
|
130
|
+
"env": {
|
|
131
|
+
"HARMONY_KB_ROOTS": "/ABS/PATH/TO/docs:/ABS/PATH/TO/proposals"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 接入 Cursor
|
|
139
|
+
|
|
140
|
+
`~/.cursor/mcp.json`(全局)或工程根 `.cursor/mcp.json`(项目级)格式同上。
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 环境变量约定
|
|
145
|
+
|
|
146
|
+
| 变量 | 默认 | 作用 |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `HARMONY_MCP_ALLOWED_ROOTS` | **空(推荐留空)** | 额外允许的工程根目录白名单(多个用 PATH 分隔符)。**v0.2 起非必填**:sandbox 会从工具入参里的 `project_dir` / `file_path` 自动向上寻找 `hvigorw` / `hvigorfile.ts` / `oh-package.json5` / `build-profile.json5` 等鸿蒙工程标志文件并加入白名单,切换工程**无需任何配置或重启**。只有当你想给非鸿蒙工程的目录预先开通访问权限,或者希望显式收紧白名单时才需要设置。 |
|
|
149
|
+
| `HARMONY_MCP_LOG_LEVEL` | `info` | 日志等级:debug/info/warn/error |
|
|
150
|
+
| `HDC_BIN` | `hdc` | hdc 可执行路径 |
|
|
151
|
+
| `HDC_TARGET` | — | 默认设备序列号 |
|
|
152
|
+
| `HVIGOR_BIN` | `hvigorw` | hvigor 可执行路径 |
|
|
153
|
+
| `HARMONY_KB_ROOTS` | `./docs:./proposals` | knowledge MCP 暴露的知识库根 |
|
|
154
|
+
| `HARMONY_ARKANALYZER_JAR` | — | 预留:外部 ArkAnalyzer JAR 接入(本期未启用) |
|
|
155
|
+
|
|
156
|
+
### 沙箱自动识别机制(v0.2)
|
|
157
|
+
|
|
158
|
+
- `harmony-hvigor`(要执行 shell 的工具):只允许操作"看起来是鸿蒙工程"的目录——即向上能找到 `hvigorw` / `hvigorfile.ts` / `build-profile.json5` / `oh-package.json5` 之一。这一限制不能被关闭,避免 Agent 误把 hvigor 跑到非工程目录。
|
|
159
|
+
- `harmony-arkcheck` / `harmony-arkanalyzer`(只读静态分析):拿到 `project_dir` / `file_path` 时优先按上面的规则识别工程根;若识别不到,会把传入的目录本身作为合法根加入沙箱,方便扫描任意 TS / ArkTS 目录。
|
|
160
|
+
- `harmony-hdc` / `harmony-profiler` / `harmony-knowledge`:本身不依赖工程沙箱(操作的是设备 / 临时 trace 文件 / 文档资源),与该变量无关。
|
|
161
|
+
|
|
162
|
+
> ⚠️ 如果只在某次终端里 `export HARMONY_MCP_ALLOWED_ROOTS=...`,重启 IDE 或新终端会失效。**推荐做法是直接什么都不设**,让自动识别工作;要持久化才考虑写进 `~/.zshrc` / IDE 的 MCP 配置 `env` 块。
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 一个典型 Agent 工作流
|
|
167
|
+
|
|
168
|
+
> 用户:在 Cursor 里说"帮我修首页冷启动卡顿"
|
|
169
|
+
|
|
170
|
+
Agent 自动编排:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
1. harmony-arkanalyzer list_components → 找到 EntryAbility / IndexPage
|
|
174
|
+
2. harmony-hdc list_devices → 选第一台设备
|
|
175
|
+
3. harmony-profiler cold_start_breakdown → force-stop → start → 抓 5s trace → 分析
|
|
176
|
+
4. (Agent 推理:发现 main 线程 readFileSync 耗时 230ms)
|
|
177
|
+
5. harmony-arkcheck explain_rule ARK004 → 拿到修复建议文案
|
|
178
|
+
6. harmony-knowledge search_kb "异步 IO" → 找到公司性能 SOP
|
|
179
|
+
7. (Agent 修改源码:readFileSync → await fs.read)
|
|
180
|
+
8. harmony-hvigor build_hap → 编译验证
|
|
181
|
+
9. harmony-hdc install_hap → 装回设备复测
|
|
182
|
+
10. harmony-profiler cold_start_breakdown → 复验,确认 jank 消失
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**全程无人工 IDE 操作。**
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 安全模型
|
|
190
|
+
|
|
191
|
+
- **沙箱**:所有"读写工程文件"的工具都受 `HARMONY_MCP_ALLOWED_ROOTS` 限制,无法逃逸。
|
|
192
|
+
- **shell 白名单**:`hdc shell` 只允许调用约定子命令(aa / hilog / hitrace / snapshot_display 等),杜绝任意命令注入。
|
|
193
|
+
- **超时强制**:每条命令都有上限(默认 60s,trace 类按 duration+15s 计算),子进程 kill -9 兜底。
|
|
194
|
+
- **stdout 协议保留**:日志统一写 stderr,stdout 仅放 MCP 协议消息。
|
|
195
|
+
- **本地化**:所有 server 在本机进程执行,**不向公网/任意第三方发送数据**。
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 内置 ArkCheck 规则(first pass)
|
|
200
|
+
|
|
201
|
+
| ID | 严重度 | 标题 |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| ARK001 | warning | 高频 build() 中使用 console.log |
|
|
204
|
+
| ARK002 | warning | ForEach 缺少 key 生成器 |
|
|
205
|
+
| ARK003 | info | 长列表使用 ForEach 而非 LazyForEach |
|
|
206
|
+
| ARK004 | error | 主线程同步 IO(readSync 等) |
|
|
207
|
+
| ARK005 | warning | 组件中 new 大对象未做缓存 |
|
|
208
|
+
| ARK006 | info | 嵌套 Stack/Column 过深(>6 层) |
|
|
209
|
+
| ARK007 | warning | Worker/TaskPool 选择不当 |
|
|
210
|
+
| ARK008 | error | 明文权限/敏感字符串硬编码 |
|
|
211
|
+
| ARK009 | info | setInterval 未在 aboutToDisappear 清理 |
|
|
212
|
+
| ARK010 | info | Image 未指定大小,整图解码 |
|
|
213
|
+
|
|
214
|
+
参考:华为《2025 HarmonyOS 高性能规则检测创新实践》、最佳实践文档。
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 与现有"四提案"的关系
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
提案一 AI MR 摘要 ← 调用 arkanalyzer.impact_analysis 自动带"影响面"
|
|
222
|
+
提案二 AI Code Review ← 调用 arkcheck + arkanalyzer 升级到"看语义"
|
|
223
|
+
提案三 AI 单测生成 ← 调用 hvigor.build_hap 实现 Self-Refine 闭环
|
|
224
|
+
新性能 Profiler AI 分析 ← 100% 基于 profiler-mcp
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**MCP 化是 1+2+3 共同的底座。**
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## 路线图
|
|
232
|
+
|
|
233
|
+
- [x] **P0** `hdc-mcp` MVP + Claude Code 端到端验证
|
|
234
|
+
- [x] **P1** `hvigor-mcp` + `profiler-mcp` 跑通"改→build→trace→分析"闭环
|
|
235
|
+
- [x] **P2** `arkanalyzer-mcp` + `arkcheck-mcp`(内置版)
|
|
236
|
+
- [x] **P3** `harmony-knowledge-mcp`(RAG)
|
|
237
|
+
- [ ] **P4** SSE transport + 多用户共享部署
|
|
238
|
+
- [ ] **P5** 接入官方 ArkAnalyzer JAR(替换内置 regex 版)
|
|
239
|
+
- [ ] **P6** ArkCheck 二进制接入(同上)
|
|
240
|
+
- [ ] **P7** profiler 升级为推理模型直读 .htrace 二进制
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 目录结构
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
scripts/mcp/
|
|
248
|
+
├── package.json # npm workspaces 根
|
|
249
|
+
├── tsconfig.base.json # 公共 TS 配置
|
|
250
|
+
├── demo/run-demo.mjs # 端到端 demo(无需鸿蒙设备)
|
|
251
|
+
└── packages/
|
|
252
|
+
├── mcp-common/ # 公共库
|
|
253
|
+
├── hdc-mcp/
|
|
254
|
+
├── hvigor-mcp/
|
|
255
|
+
├── profiler-mcp/
|
|
256
|
+
├── arkanalyzer-mcp/
|
|
257
|
+
├── arkcheck-mcp/
|
|
258
|
+
└── harmony-knowledge-mcp/
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
Apache-2.0
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* arkanalyzer-mcp: ArkTS 静态分析。
|
|
4
|
+
*
|
|
5
|
+
* 本期实现轻量版(纯 TS,无外部 JAR 依赖):
|
|
6
|
+
* - find_references 基于正则的符号引用查找
|
|
7
|
+
* - impact_analysis 给定一组改动文件,反推受影响的 .ets 页面/组件
|
|
8
|
+
* - list_components 列出工程内所有 @Component/@ComponentV2 声明
|
|
9
|
+
* - count_state_vars 统计 V1/V2 状态装饰器使用情况
|
|
10
|
+
*
|
|
11
|
+
* 当 HARMONY_ARKANALYZER_JAR 环境变量配置时,会优先调用官方 ArkAnalyzer(预留接入点)。
|
|
12
|
+
*/
|
|
13
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import fs from "node:fs/promises";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { createLogger, isMainModule, PathSandbox, registerTools, } from "@harmony-mcp/common";
|
|
20
|
+
const logger = createLogger("arkanalyzer-mcp");
|
|
21
|
+
// v0.2: 允许零配置启动;首次收到 project_dir 时自动识别工程根并加白
|
|
22
|
+
const allowedRoots = (process.env.HARMONY_MCP_ALLOWED_ROOTS ?? "")
|
|
23
|
+
.split(path.delimiter)
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
const sandbox = new PathSandbox({ allowedRoots, autoDetectProjectRoot: true });
|
|
26
|
+
const ProjectField = z.string().describe("鸿蒙工程根目录绝对路径(需在 allowedRoots 内)");
|
|
27
|
+
const FindReferencesSchema = z.object({
|
|
28
|
+
project_dir: ProjectField,
|
|
29
|
+
symbol: z.string().describe("要查找的标识符名"),
|
|
30
|
+
file_glob: z.string().default("**/*.ets")
|
|
31
|
+
.describe("文件 glob;默认 **/*.ets"),
|
|
32
|
+
});
|
|
33
|
+
const ImpactAnalysisSchema = z.object({
|
|
34
|
+
project_dir: ProjectField,
|
|
35
|
+
changed_files: z.array(z.string()).describe("本次变更的文件相对/绝对路径列表"),
|
|
36
|
+
});
|
|
37
|
+
const ListComponentsSchema = z.object({
|
|
38
|
+
project_dir: ProjectField,
|
|
39
|
+
});
|
|
40
|
+
const CountStateVarsSchema = z.object({
|
|
41
|
+
project_dir: ProjectField,
|
|
42
|
+
});
|
|
43
|
+
// ---------- 文件遍历(极简 glob,仅支持 ** 和 *) ----------
|
|
44
|
+
async function walk(dir, predicate) {
|
|
45
|
+
const out = [];
|
|
46
|
+
async function rec(d) {
|
|
47
|
+
const entries = await fs.readdir(d, { withFileTypes: true });
|
|
48
|
+
for (const e of entries) {
|
|
49
|
+
if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "build")
|
|
50
|
+
continue;
|
|
51
|
+
const full = path.join(d, e.name);
|
|
52
|
+
if (e.isDirectory())
|
|
53
|
+
await rec(full);
|
|
54
|
+
else if (predicate(full))
|
|
55
|
+
out.push(full);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
await rec(dir);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function matchExt(file, exts) {
|
|
62
|
+
return exts.some((e) => file.endsWith(e));
|
|
63
|
+
}
|
|
64
|
+
// ---------- Tools ----------
|
|
65
|
+
export const NAMESPACE = "arkanalyzer";
|
|
66
|
+
export const tools = [
|
|
67
|
+
{
|
|
68
|
+
name: "find_references",
|
|
69
|
+
description: "在工程内查找指定标识符的引用(正则匹配,含定义与使用点)",
|
|
70
|
+
schema: FindReferencesSchema,
|
|
71
|
+
handler: async (a) => {
|
|
72
|
+
const root = sandbox.ensureReadOnlyRoot(a.project_dir);
|
|
73
|
+
const files = await walk(root, (p) => matchExt(p, [".ets", ".ts"]));
|
|
74
|
+
const re = new RegExp(`\\b${escapeRegex(a.symbol)}\\b`);
|
|
75
|
+
const hits = [];
|
|
76
|
+
for (const f of files) {
|
|
77
|
+
const text = await fs.readFile(f, "utf8");
|
|
78
|
+
const lines = text.split(/\r?\n/);
|
|
79
|
+
for (let i = 0; i < lines.length; i++) {
|
|
80
|
+
if (re.test(lines[i])) {
|
|
81
|
+
hits.push({ file: path.relative(root, f), line: i + 1, text: lines[i].trim() });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { symbol: a.symbol, hits: hits.slice(0, 500), total: hits.length };
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "impact_analysis",
|
|
90
|
+
description: "基于符号引用反推:本次 changed_files 中导出的符号被哪些其他 .ets 文件引用",
|
|
91
|
+
schema: ImpactAnalysisSchema,
|
|
92
|
+
handler: async (a) => {
|
|
93
|
+
const root = sandbox.ensureReadOnlyRoot(a.project_dir);
|
|
94
|
+
// 1) 抽取每个 changed_file 的导出符号(极简:匹配 export class / export function / @Component struct Foo)
|
|
95
|
+
const exportedByFile = new Map();
|
|
96
|
+
for (const rel of a.changed_files) {
|
|
97
|
+
const abs = sandbox.resolve(path.isAbsolute(rel) ? rel : path.join(root, rel));
|
|
98
|
+
let text;
|
|
99
|
+
try {
|
|
100
|
+
text = await fs.readFile(abs, "utf8");
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const symbols = extractExports(text);
|
|
106
|
+
exportedByFile.set(path.relative(root, abs), symbols);
|
|
107
|
+
}
|
|
108
|
+
// 2) 全仓搜引用
|
|
109
|
+
const files = await walk(root, (p) => matchExt(p, [".ets", ".ts"]));
|
|
110
|
+
const impactedSet = new Map(); // changedFile -> set of impactedFiles
|
|
111
|
+
for (const f of files) {
|
|
112
|
+
const rel = path.relative(root, f);
|
|
113
|
+
if (exportedByFile.has(rel))
|
|
114
|
+
continue;
|
|
115
|
+
const text = await fs.readFile(f, "utf8");
|
|
116
|
+
for (const [changed, syms] of exportedByFile) {
|
|
117
|
+
for (const sym of syms) {
|
|
118
|
+
const re = new RegExp(`\\b${escapeRegex(sym)}\\b`);
|
|
119
|
+
if (re.test(text)) {
|
|
120
|
+
const set = impactedSet.get(changed) ?? new Set();
|
|
121
|
+
set.add(rel);
|
|
122
|
+
impactedSet.set(changed, set);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const result = Object.fromEntries([...impactedSet].map(([k, v]) => [k, [...v]]));
|
|
129
|
+
const allImpacted = new Set();
|
|
130
|
+
for (const v of impactedSet.values())
|
|
131
|
+
for (const f of v)
|
|
132
|
+
allImpacted.add(f);
|
|
133
|
+
return {
|
|
134
|
+
exported_symbols: Object.fromEntries(exportedByFile),
|
|
135
|
+
per_file_impact: result,
|
|
136
|
+
all_impacted_files: [...allImpacted],
|
|
137
|
+
total_impacted: allImpacted.size,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "list_components",
|
|
143
|
+
description: "列出工程内所有 @Component / @ComponentV2 声明(struct 名)",
|
|
144
|
+
schema: ListComponentsSchema,
|
|
145
|
+
handler: async (a) => {
|
|
146
|
+
const root = sandbox.ensureReadOnlyRoot(a.project_dir);
|
|
147
|
+
const files = await walk(root, (p) => p.endsWith(".ets"));
|
|
148
|
+
const re = /@(Component|ComponentV2|Entry)\s*[\s\S]*?struct\s+(\w+)/g;
|
|
149
|
+
const components = [];
|
|
150
|
+
for (const f of files) {
|
|
151
|
+
const text = await fs.readFile(f, "utf8");
|
|
152
|
+
let m;
|
|
153
|
+
while ((m = re.exec(text)) !== null) {
|
|
154
|
+
components.push({ file: path.relative(root, f), kind: m[1], name: m[2] });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { count: components.length, components };
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "count_state_vars",
|
|
162
|
+
description: "统计 V1 / V2 状态装饰器使用次数(@State/@Prop/@Link vs @Local/@Param/@Once)",
|
|
163
|
+
schema: CountStateVarsSchema,
|
|
164
|
+
handler: async (a) => {
|
|
165
|
+
const root = sandbox.ensureReadOnlyRoot(a.project_dir);
|
|
166
|
+
const files = await walk(root, (p) => p.endsWith(".ets"));
|
|
167
|
+
const counters = {};
|
|
168
|
+
const v1 = ["@State", "@Prop", "@Link", "@Provide", "@Consume", "@ObjectLink", "@Observed"];
|
|
169
|
+
const v2 = ["@Local", "@Param", "@Once", "@Event", "@Trace", "@ObservedV2"];
|
|
170
|
+
for (const f of files) {
|
|
171
|
+
const text = await fs.readFile(f, "utf8");
|
|
172
|
+
for (const k of [...v1, ...v2]) {
|
|
173
|
+
const cnt = (text.match(new RegExp(escapeRegex(k) + "\\b", "g")) ?? []).length;
|
|
174
|
+
counters[k] = (counters[k] ?? 0) + cnt;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const v1Total = v1.reduce((s, k) => s + (counters[k] ?? 0), 0);
|
|
178
|
+
const v2Total = v2.reduce((s, k) => s + (counters[k] ?? 0), 0);
|
|
179
|
+
return { per_decorator: counters, v1_total: v1Total, v2_total: v2Total };
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
function escapeRegex(s) {
|
|
184
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
185
|
+
}
|
|
186
|
+
function extractExports(text) {
|
|
187
|
+
const symbols = new Set();
|
|
188
|
+
const patterns = [
|
|
189
|
+
/export\s+(?:default\s+)?class\s+(\w+)/g,
|
|
190
|
+
/export\s+(?:default\s+)?function\s+(\w+)/g,
|
|
191
|
+
/export\s+(?:default\s+)?const\s+(\w+)/g,
|
|
192
|
+
/export\s+(?:default\s+)?interface\s+(\w+)/g,
|
|
193
|
+
/export\s+(?:default\s+)?enum\s+(\w+)/g,
|
|
194
|
+
/@(?:Component|ComponentV2|Entry)\s*[\s\S]*?struct\s+(\w+)/g,
|
|
195
|
+
];
|
|
196
|
+
for (const re of patterns) {
|
|
197
|
+
let m;
|
|
198
|
+
while ((m = re.exec(text)) !== null)
|
|
199
|
+
symbols.add(m[1]);
|
|
200
|
+
}
|
|
201
|
+
return [...symbols];
|
|
202
|
+
}
|
|
203
|
+
async function main() {
|
|
204
|
+
if (process.env.HARMONY_ARKANALYZER_JAR) {
|
|
205
|
+
logger.info("external ArkAnalyzer JAR detected (not yet wired in this build)", {
|
|
206
|
+
jar: process.env.HARMONY_ARKANALYZER_JAR,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const server = new Server({ name: "harmony-arkanalyzer-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
210
|
+
registerTools(server, { ListToolsRequestSchema, CallToolRequestSchema }, tools);
|
|
211
|
+
await server.connect(new StdioServerTransport());
|
|
212
|
+
logger.info("arkanalyzer-mcp started", { allowedRoots });
|
|
213
|
+
}
|
|
214
|
+
if (isMainModule(import.meta.url)) {
|
|
215
|
+
main().catch((err) => {
|
|
216
|
+
logger.error("fatal", { err: String(err) });
|
|
217
|
+
process.exit(1);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../packages/arkanalyzer-mcp/src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* arkcheck-mcp: HarmonyOS 高性能编码规则扫描。
|
|
4
|
+
*
|
|
5
|
+
* 工具:
|
|
6
|
+
* - lint_file 扫描单个文件
|
|
7
|
+
* - lint_project 扫描整个工程(默认仅 .ets)
|
|
8
|
+
* - explain_rule 返回某条规则的解释 + 修复建议
|
|
9
|
+
* - list_rules 列出全部规则元数据(供 Agent 自选启用)
|
|
10
|
+
*/
|
|
11
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import fs from "node:fs/promises";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { createLogger, HarmonyMcpError, isMainModule, PathSandbox, registerTools, } from "@harmony-mcp/common";
|
|
18
|
+
import { BUILTIN_RULES, explainRule } from "./rules.js";
|
|
19
|
+
const logger = createLogger("arkcheck-mcp");
|
|
20
|
+
// v0.2: 允许零配置启动;lint_file/lint_project 收到路径时再自动识别工程根
|
|
21
|
+
const allowedRoots = (process.env.HARMONY_MCP_ALLOWED_ROOTS ?? "")
|
|
22
|
+
.split(path.delimiter)
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
const sandbox = new PathSandbox({ allowedRoots, autoDetectProjectRoot: true });
|
|
25
|
+
function scanText(text, file, enabled) {
|
|
26
|
+
const findings = [];
|
|
27
|
+
const lines = text.split(/\r?\n/);
|
|
28
|
+
for (const rule of BUILTIN_RULES) {
|
|
29
|
+
if (enabled.size && !enabled.has(rule.id))
|
|
30
|
+
continue;
|
|
31
|
+
const m = rule.pattern.exec(text);
|
|
32
|
+
if (!m)
|
|
33
|
+
continue;
|
|
34
|
+
// 计算行号:取匹配起点
|
|
35
|
+
const upto = text.slice(0, m.index);
|
|
36
|
+
const lineNo = upto.split(/\r?\n/).length;
|
|
37
|
+
findings.push({
|
|
38
|
+
rule_id: rule.id,
|
|
39
|
+
severity: rule.severity,
|
|
40
|
+
title: rule.title,
|
|
41
|
+
file,
|
|
42
|
+
line: lineNo,
|
|
43
|
+
preview: (lines[lineNo - 1] ?? "").trim(),
|
|
44
|
+
suggestion: rule.suggestion,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return findings;
|
|
48
|
+
}
|
|
49
|
+
async function walkEts(dir) {
|
|
50
|
+
const out = [];
|
|
51
|
+
async function rec(d) {
|
|
52
|
+
const entries = await fs.readdir(d, { withFileTypes: true });
|
|
53
|
+
for (const e of entries) {
|
|
54
|
+
if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "build")
|
|
55
|
+
continue;
|
|
56
|
+
const full = path.join(d, e.name);
|
|
57
|
+
if (e.isDirectory())
|
|
58
|
+
await rec(full);
|
|
59
|
+
else if (full.endsWith(".ets") || full.endsWith(".ts"))
|
|
60
|
+
out.push(full);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
await rec(dir);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
const LintFileSchema = z.object({
|
|
67
|
+
file_path: z.string().describe("要扫描的文件绝对路径"),
|
|
68
|
+
rules: z.array(z.string()).optional().describe("仅启用这些规则 ID;省略=全部"),
|
|
69
|
+
});
|
|
70
|
+
const LintProjectSchema = z.object({
|
|
71
|
+
project_dir: z.string().describe("工程根目录"),
|
|
72
|
+
rules: z.array(z.string()).optional(),
|
|
73
|
+
max_findings: z.number().int().positive().max(2000).default(500),
|
|
74
|
+
});
|
|
75
|
+
const ExplainRuleSchema = z.object({
|
|
76
|
+
rule_id: z.string(),
|
|
77
|
+
});
|
|
78
|
+
const ListRulesSchema = z.object({});
|
|
79
|
+
export const NAMESPACE = "arkcheck";
|
|
80
|
+
export const tools = [
|
|
81
|
+
{
|
|
82
|
+
name: "lint_file",
|
|
83
|
+
description: "对单个文件运行内置规则集",
|
|
84
|
+
schema: LintFileSchema,
|
|
85
|
+
handler: async (a) => {
|
|
86
|
+
const abs = sandbox.ensureReadOnlyRoot(a.file_path);
|
|
87
|
+
const text = await fs.readFile(abs, "utf8");
|
|
88
|
+
const enabled = new Set(a.rules ?? []);
|
|
89
|
+
const findings = scanText(text, abs, enabled);
|
|
90
|
+
return { file: abs, findings, count: findings.length };
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "lint_project",
|
|
95
|
+
description: "对整个工程运行规则集,返回前 N 条 findings",
|
|
96
|
+
schema: LintProjectSchema,
|
|
97
|
+
handler: async (a) => {
|
|
98
|
+
const root = sandbox.ensureReadOnlyRoot(a.project_dir);
|
|
99
|
+
const files = await walkEts(root);
|
|
100
|
+
const enabled = new Set(a.rules ?? []);
|
|
101
|
+
const all = [];
|
|
102
|
+
for (const f of files) {
|
|
103
|
+
const text = await fs.readFile(f, "utf8");
|
|
104
|
+
const rel = path.relative(root, f);
|
|
105
|
+
all.push(...scanText(text, rel, enabled));
|
|
106
|
+
if (all.length >= a.max_findings)
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
scanned_files: files.length,
|
|
111
|
+
findings: all.slice(0, a.max_findings),
|
|
112
|
+
total: all.length,
|
|
113
|
+
by_severity: countBy(all, (x) => x.severity),
|
|
114
|
+
by_rule: countBy(all, (x) => x.rule_id),
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "explain_rule",
|
|
120
|
+
description: "返回某条规则的解释、严重度与修复建议",
|
|
121
|
+
schema: ExplainRuleSchema,
|
|
122
|
+
handler: async (a) => {
|
|
123
|
+
const r = explainRule(a.rule_id);
|
|
124
|
+
if (!r)
|
|
125
|
+
throw new HarmonyMcpError("RULE_NOT_FOUND", `unknown rule_id: ${a.rule_id}`);
|
|
126
|
+
const { pattern, ...rest } = r;
|
|
127
|
+
return { ...rest, pattern_source: pattern.source };
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "list_rules",
|
|
132
|
+
description: "列出全部内置规则元数据",
|
|
133
|
+
schema: ListRulesSchema,
|
|
134
|
+
handler: async () => BUILTIN_RULES.map(({ pattern, ...rest }) => ({ ...rest, pattern_source: pattern.source })),
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
function countBy(arr, key) {
|
|
138
|
+
const r = {};
|
|
139
|
+
for (const x of arr) {
|
|
140
|
+
const k = key(x);
|
|
141
|
+
r[k] = (r[k] ?? 0) + 1;
|
|
142
|
+
}
|
|
143
|
+
return r;
|
|
144
|
+
}
|
|
145
|
+
async function main() {
|
|
146
|
+
const server = new Server({ name: "harmony-arkcheck-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
147
|
+
registerTools(server, { ListToolsRequestSchema, CallToolRequestSchema }, tools);
|
|
148
|
+
await server.connect(new StdioServerTransport());
|
|
149
|
+
logger.info("arkcheck-mcp started", {
|
|
150
|
+
allowedRoots,
|
|
151
|
+
builtin_rules: BUILTIN_RULES.length,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (isMainModule(import.meta.url)) {
|
|
155
|
+
main().catch((err) => {
|
|
156
|
+
logger.error("fatal", { err: String(err) });
|
|
157
|
+
process.exit(1);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=index.js.map
|