ht32-code-mcp 0.4.4__tar.gz → 0.4.6__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.
Files changed (37) hide show
  1. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/DEPLOY.md +177 -2
  2. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/PKG-INFO +1 -1
  3. ht32_code_mcp-0.4.6/README.md +37 -0
  4. ht32_code_mcp-0.4.6/components.example.json +103 -0
  5. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/pyproject.toml +4 -1
  6. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/smoke_stdio.py +9 -3
  7. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/__init__.py +4 -4
  8. ht32_code_mcp-0.4.6/src/ht32_code_mcp/component_exporter.py +193 -0
  9. ht32_code_mcp-0.4.6/src/ht32_code_mcp/component_source.py +434 -0
  10. ht32_code_mcp-0.4.6/src/ht32_code_mcp/components.py +509 -0
  11. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/exporter.py +14 -5
  12. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/materializer.py +75 -4
  13. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/server.py +313 -2
  14. ht32_code_mcp-0.4.6/tests/conftest.py +155 -0
  15. ht32_code_mcp-0.4.6/tests/test_component_export.py +164 -0
  16. ht32_code_mcp-0.4.6/tests/test_component_redact.py +125 -0
  17. ht32_code_mcp-0.4.6/tests/test_component_source.py +226 -0
  18. ht32_code_mcp-0.4.6/tests/test_component_tools.py +191 -0
  19. ht32_code_mcp-0.4.6/tests/test_components.py +354 -0
  20. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/uv.lock +1 -1
  21. ht32_code_mcp-0.4.4/README.md +0 -6
  22. ht32_code_mcp-0.4.4/tests/conftest.py +0 -10
  23. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/.gitignore +0 -0
  24. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/.gitkeep +0 -0
  25. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/.pytest_base_final_run.log +0 -0
  26. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/e2e_scenario_a.py +0 -0
  27. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/e2e_scenario_b.py +0 -0
  28. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/smoke_http.py +0 -0
  29. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/__main__.py +0 -0
  30. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/index.py +0 -0
  31. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/knowledge.py +0 -0
  32. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/release.py +0 -0
  33. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/usage.py +0 -0
  34. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/tests/test_export_cloud_mode.py +0 -0
  35. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/tests/test_knowledge_grounding.py +0 -0
  36. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/tests/test_recipe_claims.py +0 -0
  37. {ht32_code_mcp-0.4.4 → ht32_code_mcp-0.4.6}/tests/test_tools.py +0 -0
@@ -330,6 +330,180 @@ chmod 755 /opt/ht32-mcp-feedback
330
330
  4. 清理策略由定时同步脚本第 7 步负责(`LOG_KEEP_DAYS=30`,与同步日志同口径,
331
331
  目录变量为 `FEEDBACK_ROOT`)。
332
332
 
333
+ ## 私有组件(0.4.5+)
334
+
335
+ 让 MCP 用户发现私有 Git 组件(BSP / 中间件 / 驱动包),检索/读取源码,并把**选定 tag 的
336
+ 源码快照**并入导出工程。组件对全部 MCP 用户可见、可导出;新增组件只改仓库内登记表,
337
+ 不改 MCP 代码或 Skill。
338
+
339
+ ```
340
+ 用户 → MCP: list_components / get_component_info / search_component_code / read_component_file
341
+ export_project_bundle(components=["<id>@<tag>"])
342
+ MCP 服务端 → 仓库内登记表(默认 .work/components/components.json)→ remotes 映射 → 私有 Git(本机路径或私有网络)
343
+ ```
344
+
345
+ ### 环境变量
346
+
347
+ | 变量 | 默认 | 说明 |
348
+ |------|------|------|
349
+ | `COMPONENTS_CONFIG` | `.work/components/components.json`(文件存在即启用) | 组件目录 JSON 路径;显式配置优先。文件存在但配置无效时**服务启动失败**(错误定位到组件 ID/字段) |
350
+ | `COMPONENTS_STATE_FILE` | `<临时目录>/ht32-component-cache/tag-lock.json` | tag→commit 台账(检测 tag 被强制移动);生产建议放持久卷(compose 已指向 `/app/feedback/component-tag-lock.json`) |
351
+ | `COMPONENT_GIT_TIMEOUT` | `60` | 单次 Git 操作超时(秒) |
352
+
353
+ **运行时依赖 `git` + 私有仓库凭据**(两者缺一,组件工具都会返回
354
+ `[COMPONENT_REMOTE_UNAVAILABLE]`):
355
+
356
+ 1. **镜像**:官方 `supercorp/supergateway:next-uvx` 默认不带 git。同目录
357
+ [`.work/docker/Dockerfile`](../docker/Dockerfile) 在其上补装 git(自动适配
358
+ apt/apk),compose 已改为 `image: ht32-code-mcp:0.4.5-git` + `build: .`:
359
+
360
+ ```bash
361
+ cd /vol1/1000/SSD/docker/ht32-code/.work/docker
362
+ docker compose build ht32-code-mcp # 首次;之后同步脚本带 --build 会自动复用缓存
363
+ docker compose up -d --force-recreate ht32-code-mcp
364
+ docker exec ht32-code-mcp git --version # 必须打印版本
365
+ ```
366
+
367
+ 2. **凭据**:内网 Gitea 仓库非公开,匿名 `git ls-remote` 返回 **403**(可用
368
+ `curl -i http://100.66.1.6:3000/api/v1/repos/S621/HT32_BSP/tags` 复核)。在部署机
369
+ `.work/docker/.env`(已被 `.gitignore` 忽略)写入 Basic 凭据:
370
+
371
+ ```bash
372
+ # Gitea → 设置 → 应用 → 生成令牌,权限只需 read:repository
373
+ printf 'COMPONENTS_GIT_AUTH=%s\n' \
374
+ "$(printf '%s:%s' '<Gitea 用户名>' '<只读 token>' | base64 -w0)" > .env
375
+ chmod 600 .env
376
+ ```
377
+
378
+ compose 以 `GIT_CONFIG_COUNT/KEY_0/VALUE_0` 注入 `http.extraheader`,只存在于
379
+ 容器进程环境;不写入仓库、日志或 MCP 响应。轮换 token 后改 `.env` 并重建容器。
380
+
381
+ 3. **验证**:容器内直接执行(无需再带 `-c`,凭据由环境注入):
382
+
383
+ ```bash
384
+ docker exec ht32-code-mcp git ls-remote http://100.66.1.6:3000/S621/LwFSM.git | tail -2
385
+ ```
386
+
387
+ 能列出 `refs/tags/V2.0` 即表示 git 与凭据都就位。纯检索/导出模板不依赖 git,不受影响。
388
+
389
+ ### 源码裁剪(`export.redact`,0.4.6+)
390
+
391
+ 面向“只交付 `.lib`、不给源码”的组件。**不执行仓库里的 `script/submodule.bat`**
392
+ (Linux 无 cmd,且执行仓库脚本等于远程执行),改为快照层的等价内置规则:
393
+
394
+ | 模式 | 行为 |
395
+ |---|---|
396
+ | `none`(默认) | 不裁剪 |
397
+ | `auto` | 仅当仓库含 `script/submodule.bat` 时,按下面的规则裁剪 |
398
+ | `lib-only` | 无条件裁剪 |
399
+
400
+ `lib-only` 规则(路径相对导出根,大小写不敏感):
401
+
402
+ 1. 移除根目录 `project/`;
403
+ 2. `lib/<name>.lib` 存在 → 移除 `src/<name>.c`;
404
+ 3. `tools/InfoProcess.exe` 存在 → 移除 `tools/InfoProcess.c`;
405
+ 4. `script/` 保留。
406
+
407
+ 裁剪发生在**快照构建阶段**,因此 `read_component_file`、`search_component_code`
408
+ 与导出都看不到被移除的源文件;`manifest/components-lock.json` 会记录
409
+ `redaction.mode` 与逐条 `removed`(路径 + 原因),便于审计。
410
+
411
+ ### 组件登记表放在哪里
412
+
413
+ **放在 ht32-code 仓库里**:`.work/components/components.json`,随仓库提交、回滚、blame。
414
+
415
+ ```
416
+ ht32-code(私有仓库,服务器 /vol1/1000/SSD/docker/ht32-code → 容器 /app/code)
417
+ └─ .work/components/components.json ← 登记表(真实 remote 地址在这里)
418
+ ```
419
+
420
+ - **好处**:新增组件 = 一次 commit;服务器更新脚本 `git fetch + git reset --hard origin/master`
421
+ 后自动生效;历史用 `git log -p .work/components/components.json` 查,回滚用
422
+ `git checkout <commit> -- .work/components/components.json`;**不需要发 MCP 版本**。
423
+ - **不进 PyPI**:wheel 只打包 `src/ht32_code_mcp`;sdist 已在
424
+ `pyproject.toml` 的 `[tool.hatch.build.targets.sdist].exclude` 排除 `.work/components`。
425
+ 发布前可 `uv build` 后检查 `tar -tzf dist/*.tar.gz | grep components` 应为空。
426
+ - **前提:仓库必须保持私有**。若 ht32-code 将来转为公开,必须先把登记表移出仓库
427
+ (改回私有目录挂载或独立私有配置仓库),否则组件清单与内网地址会随仓库泄露。
428
+ - 登记表可选写 `catalog_version`(人工标签);服务端另算 `catalog_sha256`(文件内容哈希)
429
+ 与所在仓库 `commit`,三者一起出现在 `get_code_version` / `list_components` /
430
+ `components-lock.json`,用于反查“这次调用是哪一版登记表”。
431
+ - 登记字段要点:`series` 按**族前缀**匹配(登记 `HT32F5` 即覆盖 `HT32F50241`
432
+ 等同族型号,也支持 `HT32F49*` 通配);`["*"]` 表示已确认覆盖全部型号;
433
+ `chips` 只用于展示“已验证型号”,不必穷举同族全部 IC;`list_components(query=...)`
434
+ 也接受型号。
435
+ - 加载器**支持 `//` 与 `/* */` 注释**(字符串里的 `//` 不受影响;不支持尾随逗号);
436
+ 逐字段说明见带注释的 `components.example.json`,**正式登记表建议保持纯 JSON**。
437
+ 注释计入文件内容哈希——改注释等同于换了一版登记表。
438
+
439
+ ### 服务器侧落地
440
+
441
+ **第 1 步:在仓库里提交登记表**
442
+
443
+ ```powershell
444
+ # 维护者本地(ht32-code 仓库)
445
+ mkdir -Path .work\components
446
+ # 写入 components.json(结构见 components.example.json)
447
+ git add .work/components/components.json
448
+ git commit -m "feat(components): 登记 HT32_BSP 与 LwFSM"
449
+ git push
450
+ ```
451
+
452
+ **第 2 步:服务器同步 + 重建**
453
+
454
+ ```bash
455
+ # 手动:仓库已挂载到 /app/code,git reset 后重启容器即可
456
+ cd /vol1/1000/SSD/docker/ht32-code && git fetch && git reset --hard origin/master
457
+ docker compose -f .work/docker/docker-compose.yml up -d --force-recreate
458
+ # 或等 1Panel 计划任务里的 ht32-code-mcp-update.md(每天 02:30)自动执行
459
+ ```
460
+
461
+ 不需要新建 `/app/components`、不需要额外挂载;`COMPONENTS_CONFIG` 已在 compose 指向
462
+ `/app/code/.work/components/components.json`(即使不配,服务也会在默认位置发现它)。
463
+
464
+ **第 3 步:验证**
465
+
466
+ ```bash
467
+ docker logs ht32-code-mcp | grep "私有组件目录"
468
+ # [ht32-code-mcp] 私有组件目录: 2 个组件(catalog 3f9a1c2b / commit 7f43dfc4 / tag 台账 ...)
469
+ ```
470
+
471
+ 配置无效时进程以退出码 2 结束,日志给出 `[COMPONENTS_CONFIG_INVALID] <字段>`。
472
+
473
+ **第 4 步:工具验证**
474
+
475
+ 1. `get_code_version` 出现 `私有组件目录:已登记 N 个组件(catalog … / commit …)`。
476
+ 2. `list_components()` 列出登记组件与可用 tag(远端不可用时显示
477
+ `远端不可用([COMPONENT_REMOTE_UNAVAILABLE] …)`,不回退 branch/HEAD)。
478
+ 3. `export_project_bundle(config_id=..., components=["<id>@<tag>"])` 的产物含
479
+ `components/<id>/` 与 `manifest/components-lock.json`(tag/commit/逐文件 SHA256,
480
+ 以及 `catalog.sha256/commit/version` 登记表来源)。
481
+
482
+ ### 日常运维
483
+
484
+ | 操作 | 做法 |
485
+ |---|---|
486
+ | 新增组件 | 编辑 `.work/components/components.json` → commit/push → 同步脚本(或手动 reset + 重建);**不改 Skills、不发 MCP 版本** |
487
+ | 刷新可用 tag | 组件维护者打 tag 即可;登记规则匹配的新 tag 自动出现(`git ls-remote` 每次查询) |
488
+ | 下架/暂停组件 | 该条 `"status": "disabled"` → commit → 同步;列表显示为已停用,读取/导出明确拒绝 |
489
+ | 回滚登记表 | `git checkout <commit> -- .work/components/components.json` → commit/push → 同步;或 `git revert <commit>` |
490
+ | tag 漂移 | 同 tag 指向新 commit 时返回 `[COMPONENT_TAG_DRIFT]`(阻断);维护者核实后删除台账里该行再重试 |
491
+ | 查历史 | `git log -p -- .work/components/components.json`;工程侧看 `components-lock.json` 的 `catalog.commit` |
492
+ | **不要**在服务器上直接改 | 更新脚本是 `git reset --hard`,手改会被覆盖;所有变更走仓库提交 |
493
+ | 临时目录 | 每次读取/导出的浅克隆在系统临时目录,成功后立即清理 |
494
+ | Git 不可达 | 短超时 + `[COMPONENT_REMOTE_UNAVAILABLE]`(错误信息已脱敏,不含 remote URL) |
495
+
496
+ ### 隐私/安全检查(发布或改配置后)
497
+
498
+ ```bash
499
+ # 1) 公开产物(PyPI 包)不得含登记表
500
+ uv build && tar -tzf dist/*.tar.gz | grep -i "\.work/components" && echo "泄漏!" || echo "OK"
501
+ # wheel 只含 src/,可用 unzip -l dist/*.whl | grep components 复核
502
+
503
+ # 2) 登记表本身不得含凭据(URL 可以,token/密码不行)
504
+ git grep -nE "PRIVATE-TOKEN|password=|ssh://.*@" -- .work/components
505
+ ```
506
+
333
507
  ## PyPI 包发布流程
334
508
 
335
509
  ```bash
@@ -361,7 +535,7 @@ uv lock && uv build && uv publish
361
535
 
362
536
  | # | 任务 | 说明 | 状态 |
363
537
  |---|------|------|------|
364
- | 3.1 | MCP 工具实现(7 检索/导出 + 1 反馈) | `.work/mcp-server/src/ht32_code_mcp/`(FastMCP) | ✅ |
538
+ | 3.1 | MCP 工具实现(7 检索/导出 + 4 私有组件 + 版本检查 + 1 反馈) | `.work/mcp-server/src/ht32_code_mcp/`(FastMCP) | ✅ |
365
539
  | 3.2 | 工具级测试 | `pytest` 46 用例全绿 | ✅ |
366
540
  | 3.3 | PyPI 发布(hatchling) | `ht32-code-mcp` 0.2.3(2026-09-24,含全系列固件库覆盖 + 6 模板多模板导出) | ✅ |
367
541
  | 3.4 | 云端 git clone + 挂载 | `/opt/ht32-code:/app/code` | ✅ |
@@ -369,4 +543,5 @@ uv lock && uv build && uv publish
369
543
  | 3.6 | OpenResty 超时修复(`proxy_read_timeout 3600s`) | 修复 60s 空闲断连;2026-09-21 复核两端空闲 100s 后调用仍 200,未被 1Panel 回退 | ✅ |
370
544
  | 3.7 | 云端 zip 下载通道(P1-3) | 服务器侧已上线:下载 1608157 字节 / SHA256 一致 / 解压 455 文件 / UV4 0E0W | ✅ |
371
545
  | 3.8 | 定时同步脚本含 bundle 清理(P1-2) | `ht32-code-mcp-update.md` 第 6 步 `find … -mmin +60 -delete` | ✅ |
372
- | 3.9 | 反馈与用量日志(P1-5,与 wiki 统一) | 0.2.1;云端已启用,但 `FEEDBACK_ROOT` 漏写前导 `/`(显示 `app/feedback/code`),需改绝对路径 | ⚠️ |
546
+ | 3.9 | 反馈与用量日志(P1-5,与 wiki 统一) | 0.2.1;云端已启用,但 `FEEDBACK_ROOT` 漏写前导 `/`(显示 `app/feedback/code`),需改绝对路径 | ⚠️ |
547
+ | 3.10 | 私有组件(0.4.5) | 登记表随仓库版本化;镜像 `ht32-code-mcp:0.4.5-git`(内置 git)+ 只读凭据注入;2026-11-02 线上验收:2 组件真实 tag、读/搜、导出 601 条目 / 组件 94 文件 / 无 `.git` | ✅ |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ht32-code-mcp
3
- Version: 0.4.4
3
+ Version: 0.4.6
4
4
  Summary: HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出
5
5
  License-Expression: MIT
6
6
  Keywords: firmware,holtek,ht32,mcp,mcu
@@ -0,0 +1,37 @@
1
+ # .work/mcp-server/
2
+
3
+ MCP server 代码目录(Python + FastMCP),实现 11 个检索/导出工具 + 版本检查 + 1 个反馈工具。
4
+
5
+ 传输由 `MCP_TRANSPORT` 决定:`stdio`(默认,供 supergateway 包装)/ `streamable-http` / `sse`。
6
+ 部署与排错见 [DEPLOY.md](DEPLOY.md)。
7
+
8
+ ## 私有组件(0.4.5+)
9
+
10
+ 登记在**ht32-code 仓库**里的私有组件(BSP / 中间件 / 驱动包),全部对 MCP 用户
11
+ 可见、可检索、可导出;新增组件只需改登记文件并提交,不改 MCP 代码或 Skill。
12
+
13
+ | 环境变量 | 默认 | 说明 |
14
+ |---|---|---|
15
+ | `COMPONENTS_CONFIG` | `.work/components/components.json`(存在即启用) | 组件目录 JSON 路径;显式配置优先,配置无效时服务启动失败 |
16
+ | `COMPONENTS_STATE_FILE` | `<临时目录>/ht32-component-cache/tag-lock.json` | tag→commit 台账,检测 tag 被强制移动(生产建议放持久卷) |
17
+ | `COMPONENT_GIT_TIMEOUT` | `60` | 单次 Git 操作超时(秒) |
18
+
19
+ - **登记表随仓库版本化**:`.work/components/components.json` 进 git(历史/回滚/blame),
20
+ 服务器更新脚本 `git reset --hard` 后自动生效;**不随 PyPI 包发布**(wheel 只打
21
+ `src/`,sdist 已排除 `.work/components`)。
22
+ - 加载器支持 `//` 与 `/* */` 注释(字符串里的 `//` 不受影响;不支持尾随逗号);
23
+ 字段说明见带注释的 [components.example.json](components.example.json),**正式登记表
24
+ 建议保持纯 JSON**。注释计入 `catalog_sha256`——改注释等同换一版登记表。
25
+ - 登记表可写 `catalog_version` 人工标签;服务端另记录内容哈希 `catalog_sha256` 与
26
+ 所在仓库 commit,工具输出、`components-lock.json` 与日志都可反查到具体版本。
27
+ - 公开仓库只放 schema 样例 [components.example.json](components.example.json);
28
+ **若 ht32-code 仓库转为公开,必须先把真实登记表移出**(改用私有目录/私有仓库)。
29
+ - 工具:`list_components` / `get_component_info` / `search_component_code` /
30
+ `read_component_file`;导出走 `export_project_bundle(components=["<id>@<tag>"])`。
31
+ `list_components(series=...)` 按**系列族前缀**匹配:登记的 `HT32F5` 可命中
32
+ `HT32F50241` 等同族任意型号(也支持通配符如 `HT32F49*`),无需逐颗枚举;
33
+ 与 MCU 无关或已确认全覆盖的组件登记 `["*"]` 即对全部型号可见。
34
+ - 快照只取指定 tag 的对象(`git fetch --depth 1` + `git archive`),**不含 `.git`/历史**;
35
+ 路径穿越、符号链接、凭据类文件、容量超限与目标冲突都会拒绝,锁定记录写入
36
+ `manifest/components-lock.json`。
37
+ - 用户只能选登记规则允许的 tag;未知/不允许的 tag 直接报错,不回退 branch/HEAD。
@@ -0,0 +1,103 @@
1
+ {
2
+ // ============================================================
3
+ // 组件登记表样例(复制到 .work/components/components.json 后改成真实值)
4
+ // - 支持 // 与 /* */ 注释,但不支持尾随逗号(去掉注释后是严格 JSON)
5
+ // - 正式登记表建议保持纯 JSON;本样例用于逐字段说明
6
+ // - 所有登记组件对全部 MCP 用户可见、可导出;没有按用户/团队的权限字段
7
+ // - 字段完整说明见 .work/mcp-server/DEPLOY.md「私有组件目录」
8
+ // ============================================================
9
+
10
+ // 字段结构版本;只有 schema 变化时才改(当前固定 1)
11
+ "schema_version": 1,
12
+
13
+ // 人工版本标签,仅用于人读/回溯;服务端还会自动记录内容哈希与 git commit
14
+ "catalog_version": "2026-11-02.1",
15
+
16
+ // 远端映射:组件条目只引用这里的“逻辑键”,真实地址不会出现在 MCP 响应里。
17
+ // 可以是 http(s)://、ssh:// 或服务器本机路径。
18
+ "remotes": {
19
+ "gitea-main": { "url": "https://git.example.invalid/ht32/REPLACE-ME.git" }
20
+ },
21
+
22
+ "components": [
23
+ {
24
+ // 唯一 ID:默认导出到工程内 components/<id>/(全小写,可用下划线/连字符)
25
+ "id": "ht32_bsp",
26
+ // 显示名(列表与详情里给人看)
27
+ "name": "HT32 BSP",
28
+ // 列表里的一行摘要
29
+ "summary": "HT32 板级支持包(GPIO/时钟/UART 等板级初始化)",
30
+ // 详情说明:依赖、限制、使用注意写这里
31
+ "description": "示例条目:复制到 .work/components/components.json 后填入真实 remote、series/chips 与 tag 规则。",
32
+ // 适用系列(list_components(series=...) 过滤):支持族前缀——
33
+ // 登记 HT32F5 即可命中 HT32F50241 等同族任意型号;也可写通配符 HT32F49*
34
+ "series": ["HT32F5", "HT32L5", "HT32F1"],
35
+ // 可选:已验证型号(仅作展示与精确命中;不必穷举同族全部 IC)
36
+ "chips": [],
37
+ // 用途标签:bsp / middleware / driver(list_components(purpose=...) 精确匹配)
38
+ "purpose": ["bsp"],
39
+ // 引用上面 remotes 里的逻辑键(不是 URL)
40
+ "remote": "gitea-main",
41
+ // tag 白名单:只有匹配 include 且不匹配 exclude 的 tag 才允许读取/导出
42
+ "tags": {
43
+ "include": ["v*"],
44
+ "exclude": ["*-rc*", "*-beta*"]
45
+ },
46
+ // 用户不指定 tag 时用的默认 tag;必须真实存在且在允许集合内
47
+ "default_tag": "v1.0.0",
48
+ "export": {
49
+ // 只导出仓库的这个子目录;空 = 整个仓库(.git/凭据类文件永远排除)
50
+ "root": "",
51
+ // 工程内落点;空 = components/<id>
52
+ "target": "",
53
+ // 额外排除(目录前缀或通配符),如 ["docs/", "tests/", "*.pdf"]
54
+ "exclude": ["docs/", "tests/"],
55
+ // 非空时只允许这些后缀,如 [".c", ".h"];空 = 不限
56
+ "allowed_suffixes": [],
57
+ // 源码裁剪(不执行仓库脚本):none / auto / lib-only
58
+ // none = 不裁剪(默认)
59
+ // auto = 仅当仓库含 script/submodule.bat 时按下面的内置规则裁剪
60
+ // lib-only= 无条件裁剪:移除 project/、删除有同名 lib/*.lib 的 src/*.c、
61
+ // 删除与 tools/InfoProcess.exe 同名的 .c;script/ 保留
62
+ // 裁剪在快照层生效,read/search/export 都看不到被移除的源码
63
+ "redact": "none",
64
+ // 容量上限:文件数 / 单文件字节 / 总字节
65
+ "max_files": 2000,
66
+ "max_file_bytes": 2097152,
67
+ "max_total_bytes": 67108864
68
+ },
69
+ // 许可(必填)与维护者(可选)
70
+ "license": "internal-only",
71
+ "maintainer": "维护者(部署时填写)",
72
+ // active = 正常;disabled = 不列出、不可读取/导出
73
+ "status": "active"
74
+ },
75
+ {
76
+ "id": "lwfsm",
77
+ "name": "LwFSM",
78
+ "summary": "轻量状态机(应用层中间件)",
79
+ "description": "示例条目:填入真实 remote 与 tag 规则(位置同 ht32_bsp)。",
80
+ // 适用系列:支持族前缀(HT32F5 命中 HT32F50241)、通配符(HT32F49*);
81
+ // 已确认覆盖全部型号的组件写 ["*"](组件内部自行说明平台差异)
82
+ "series": ["*"],
83
+ "purpose": ["middleware"],
84
+ "remote": "gitea-main",
85
+ "tags": {
86
+ "include": ["v*"]
87
+ },
88
+ "default_tag": "v1.0.0",
89
+ "export": {
90
+ // 只取仓库里的 include/ 子目录
91
+ "root": "include",
92
+ "target": "components/lwfsm",
93
+ "exclude": [],
94
+ "max_files": 500,
95
+ "max_file_bytes": 1048576,
96
+ "max_total_bytes": 16777216
97
+ },
98
+ "license": "internal-only",
99
+ "maintainer": "维护者(部署时填写)",
100
+ "status": "active"
101
+ }
102
+ ]
103
+ }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ht32-code-mcp"
7
- version = "0.4.4"
7
+ version = "0.4.6"
8
8
  description = "HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出"
9
9
  requires-python = ">=3.11"
10
10
  license = "MIT"
@@ -42,6 +42,9 @@ exclude = [
42
42
  ".pytest_base_final",
43
43
  "audit",
44
44
  "dist",
45
+ # 私有组件登记表随 ht32-code 仓库版本化,但**不得随 PyPI 包发布**
46
+ # (含私有仓库地址/组件清单;轮子只打包 src/,sdist 需显式排除)
47
+ ".work/components",
45
48
  "**/__pycache__",
46
49
  "**/*.pyc",
47
50
  ]
@@ -1,4 +1,4 @@
1
- """stdio 冒烟测试:以子进程 stdio 启动 server,验证 7 工具全部可调用。
1
+ """stdio 冒烟测试:以子进程 stdio 启动 server,验证检索/导出/组件工具可调用。
2
2
 
3
3
  用法(从 .work/mcp-server/ 目录,需已安装依赖):
4
4
  python smoke_stdio.py
@@ -20,7 +20,8 @@ from fastmcp.client.transports import StdioTransport # noqa: E402
20
20
  EXPECTED_TOOLS = {
21
21
  "get_code_version", "list_libraries", "find_example", "get_file",
22
22
  "search_code_knowledge", "get_project_manifest", "export_project_bundle",
23
- "check_skill_update",
23
+ "list_components", "get_component_info", "search_component_code",
24
+ "read_component_file", "check_skill_update", "submit_feedback",
24
25
  }
25
26
 
26
27
 
@@ -35,11 +36,16 @@ async def run() -> int:
35
36
  if missing:
36
37
  print(f"❌ 缺失工具:{missing}")
37
38
  return 1
38
- print(f"✅ 7 工具注册齐全")
39
+ print(f"✅ {len(EXPECTED_TOOLS)} 工具注册齐全")
39
40
 
40
41
  r = await client.call_tool("get_code_version", {})
41
42
  print("✅ get_code_version:", (r.content[0].text or "").splitlines()[0])
42
43
 
44
+ r = await client.call_tool("list_components", {})
45
+ comp_text = r.content[0].text or ""
46
+ assert "组件目录" in comp_text, comp_text
47
+ print("✅ list_components:", comp_text.splitlines()[0])
48
+
43
49
  r = await client.call_tool("find_example", {"peripheral": "GPIO", "limit": 3})
44
50
  text = r.content[0].text or ""
45
51
  assert "example/GPIO" in text, text
@@ -1,4 +1,4 @@
1
- """ht32-code-mcp:HT32 代码知识库 MCP Server(7 检索/导出工具 + 1 反馈工具)。
2
-
3
- 传输:stdio(默认)/ streamable-http / sse,由 MCP_TRANSPORT 决定。
4
- """
1
+ """ht32-code-mcp:HT32 代码知识库 MCP Server(7 检索/导出 + 4 私有组件 + 版本检查 + 1 反馈)。
2
+
3
+ 传输:stdio(默认)/ streamable-http / sse,由 MCP_TRANSPORT 决定。
4
+ """
@@ -0,0 +1,193 @@
1
+ """私有组件导出:选定 tag 的源码快照(安全检查 + 锁定记录)。
2
+
3
+ 本模块只做两件事:
4
+ 1. `ComponentService`:以服务端组件目录 + 受限 Git 源为唯一入口,供 MCP 工具查询、
5
+ 检索、读取与导出;
6
+ 2. `PreparedComponents`:把用户选择的 `component_id@tag` 解析成待并入工程的源码树,
7
+ 生成 `manifest/components-lock.json` 所需的锁定记录。
8
+
9
+ 快照内容来自 `component_source`(`git archive` 的 blob 原始字节),因此不含 `.git`、
10
+ 提交历史、远端 URL 或凭据;路径穿越、符号链接、越界与超限在准备阶段即拒绝,
11
+ 失败不会留下半成品工程。
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from contextlib import contextmanager
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from typing import Callable, Iterator
20
+
21
+ from .components import (COMPONENTS_CONFIG_INVALID, COMPONENTS_NOT_CONFIGURED,
22
+ SCHEMA_VERSION, ComponentCatalog, ComponentEntry,
23
+ ComponentError)
24
+ from .component_source import (ComponentSource, Snapshot, SnapshotFile, TagInfo,
25
+ TagLockStore, open_source)
26
+
27
+ LOCK_FILENAME = "components-lock.json"
28
+ LOCK_SCHEMA_VERSION = 1
29
+
30
+
31
+ class PreparedComponents:
32
+ """一次导出中已准备就绪的组件快照(必须 close 以清理临时目录)。"""
33
+
34
+ def __init__(self, items: list["PreparedComponent"], catalog: ComponentCatalog) -> None:
35
+ self.items = items
36
+ self.catalog = catalog
37
+
38
+ def close(self) -> None:
39
+ for item in self.items:
40
+ item.snapshot.close()
41
+
42
+ @property
43
+ def file_count(self) -> int:
44
+ return sum(len(i.snapshot.files) for i in self.items)
45
+
46
+ @property
47
+ def total_bytes(self) -> int:
48
+ return sum(i.snapshot.total_bytes for i in self.items)
49
+
50
+ def lock_document(self) -> dict:
51
+ rows = []
52
+ for item in self.items:
53
+ snap = item.snapshot
54
+ rows.append({
55
+ "component_id": item.entry.id,
56
+ "name": item.entry.name,
57
+ "tag": snap.tag,
58
+ "commit": snap.commit,
59
+ "target": item.target,
60
+ "export_root": item.entry.export.root,
61
+ "file_count": len(snap.files),
62
+ "bytes": snap.total_bytes,
63
+ "tree_sha256": snap.tree_sha256,
64
+ "files": [{"path": f.rel, "bytes": f.size, "sha256": f.sha256}
65
+ for f in snap.files],
66
+ "excluded": snap.excluded,
67
+ "redaction": {"mode": snap.redact_mode, "removed": snap.redacted},
68
+ "license": item.entry.license,
69
+ "maintainer": item.entry.maintainer,
70
+ })
71
+ return {
72
+ "schema_version": LOCK_SCHEMA_VERSION,
73
+ "catalog_schema_version": SCHEMA_VERSION,
74
+ # 登记表来源:把工程反查到“哪一版组件目录”产出的
75
+ "catalog": self.catalog.provenance(),
76
+ "generated_by": "ht32-code-mcp",
77
+ "component_count": len(rows),
78
+ "components": rows,
79
+ }
80
+
81
+ def lock_json(self) -> str:
82
+ return json.dumps(self.lock_document(), ensure_ascii=False, indent=1)
83
+
84
+
85
+ @dataclass
86
+ class PreparedComponent:
87
+ entry: ComponentEntry
88
+ snapshot: Snapshot
89
+ target: str
90
+
91
+
92
+ @dataclass
93
+ class ComponentService:
94
+ """组件查询/读取/导出的统一入口(组件目录 + 受限 Git 源)。"""
95
+
96
+ catalog_provider: Callable[[], ComponentCatalog | None]
97
+ work_root: Path | None = None
98
+ timeout: int = 60
99
+ tag_lock_path: Path | None = None
100
+ _tag_locks: TagLockStore = field(init=False)
101
+
102
+ def __post_init__(self) -> None:
103
+ self._tag_locks = TagLockStore(self.tag_lock_path)
104
+
105
+ # ---------------------------------------------------------- 目录
106
+ def catalog(self) -> ComponentCatalog:
107
+ catalog = self.catalog_provider()
108
+ if catalog is None:
109
+ raise ComponentError(
110
+ COMPONENTS_NOT_CONFIGURED,
111
+ "未找到组件登记表:默认位置 .work/components/components.json 不存在,"
112
+ "且未配置 COMPONENTS_CONFIG;组件发现/检索/导出不可用")
113
+ return catalog
114
+
115
+ def entry(self, component_id: str) -> ComponentEntry:
116
+ return self.catalog().get(component_id)
117
+
118
+ def source(self, entry: ComponentEntry) -> ComponentSource:
119
+ return open_source(entry, self.catalog().remote_url(entry.remote),
120
+ work_root=self.work_root, timeout=self.timeout,
121
+ tag_locks=self._tag_locks)
122
+
123
+ def tags(self, entry: ComponentEntry) -> tuple[list[TagInfo] | None, str]:
124
+ """返回 (tag 列表, 失败原因)。失败不抛异常,供列表工具降级展示。"""
125
+ try:
126
+ return self.source(entry).list_tags(), ""
127
+ except ComponentError as e:
128
+ return None, f"[{e.code}] {e.message}"
129
+
130
+ # ---------------------------------------------------------- 读取
131
+ @contextmanager
132
+ def snapshot(self, component_id: str, tag: str = "") -> Iterator[Snapshot]:
133
+ entry = self.entry(component_id)
134
+ snap = self.source(entry).snapshot(tag)
135
+ try:
136
+ yield snap
137
+ finally:
138
+ snap.close()
139
+
140
+ # ---------------------------------------------------------- 导出前准备
141
+ def prepare(self, selections: list[str]) -> PreparedComponents | None:
142
+ """`["ht32_bsp@v1.2.0", ...]` → 待并入工程的快照集合。
143
+
144
+ 未提供选择时返回 None(工程导出行为与旧版完全一致);任一选择无效即整体失败。
145
+ """
146
+ items = [s.strip() for s in (selections or []) if s and s.strip()]
147
+ if not items:
148
+ return None
149
+ catalog = self.catalog()
150
+ prepared: list[PreparedComponent] = []
151
+ seen: set[str] = set()
152
+ try:
153
+ for spec in items:
154
+ cid, _, tag = spec.partition("@")
155
+ cid, tag = cid.strip(), tag.strip()
156
+ entry = catalog.get(cid)
157
+ if entry.id in seen:
158
+ raise ComponentError(
159
+ COMPONENTS_CONFIG_INVALID,
160
+ f"组件 '{cid}' 在本次导出中被重复选择(每个组件只能选一个 tag)")
161
+ seen.add(entry.id)
162
+ snap = self.source(entry).snapshot(tag)
163
+ prepared.append(PreparedComponent(entry=entry, snapshot=snap,
164
+ target=entry.target))
165
+ except BaseException:
166
+ for item in prepared:
167
+ item.snapshot.close()
168
+ raise
169
+ return PreparedComponents(prepared, catalog)
170
+
171
+
172
+ # ------------------------------------------------------------------ 工具输出辅助
173
+ def describe_tags(tags: list[TagInfo] | None, error: str, limit: int = 20) -> list[str]:
174
+ if tags is None:
175
+ return [f" 可用 tag:远端不可用({error})"]
176
+ if not tags:
177
+ return [" 可用 tag:(登记规则下无匹配 tag)"]
178
+ lines = [" 可用 tag:"]
179
+ for info in tags[:limit]:
180
+ lines.append(f" - {info.tag}(commit {info.short_commit})")
181
+ if len(tags) > limit:
182
+ lines.append(f" … 另有 {len(tags) - limit} 个,用 get_component_info 查看")
183
+ return lines
184
+
185
+
186
+ def component_manifest_entries(
187
+ prepared: PreparedComponents) -> list[tuple[str, PreparedComponent, SnapshotFile]]:
188
+ """待并入工程的文件清单:`[(工程内相对路径, 组件, 快照文件)]`。"""
189
+ out: list[tuple[str, PreparedComponent, SnapshotFile]] = []
190
+ for item in prepared.items:
191
+ for f in item.snapshot.files:
192
+ out.append((f"{item.target}/{f.rel}", item, f))
193
+ return out