ht32-code-mcp 0.2.6__tar.gz → 0.3.0__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 (25) hide show
  1. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/PKG-INFO +1 -1
  2. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/e2e_scenario_a.py +2 -2
  3. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/pyproject.toml +1 -1
  4. ht32_code_mcp-0.3.0/src/ht32_code_mcp/exporter.py +181 -0
  5. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/src/ht32_code_mcp/index.py +5 -4
  6. ht32_code_mcp-0.3.0/src/ht32_code_mcp/materializer.py +919 -0
  7. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/src/ht32_code_mcp/server.py +82 -47
  8. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/tests/test_knowledge_grounding.py +2 -2
  9. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/tests/test_tools.py +217 -315
  10. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/uv.lock +1 -1
  11. ht32_code_mcp-0.2.6/src/ht32_code_mcp/exporter.py +0 -472
  12. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/.gitignore +0 -0
  13. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/.gitkeep +0 -0
  14. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/.pytest_base_final_run.log +0 -0
  15. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/DEPLOY.md +0 -0
  16. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/README.md +0 -0
  17. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/e2e_scenario_b.py +0 -0
  18. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/smoke_http.py +0 -0
  19. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/smoke_stdio.py +0 -0
  20. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/src/ht32_code_mcp/__init__.py +0 -0
  21. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/src/ht32_code_mcp/__main__.py +0 -0
  22. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/src/ht32_code_mcp/knowledge.py +0 -0
  23. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/src/ht32_code_mcp/release.py +0 -0
  24. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/src/ht32_code_mcp/usage.py +0 -0
  25. {ht32_code_mcp-0.2.6 → ht32_code_mcp-0.3.0}/tests/conftest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ht32-code-mcp
3
- Version: 0.2.6
3
+ Version: 0.3.0
4
4
  Summary: HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出
5
5
  License-Expression: MIT
6
6
  Keywords: firmware,holtek,ht32,mcp,mcu
@@ -69,8 +69,8 @@ def wiki_transport() -> StdioTransport:
69
69
 
70
70
 
71
71
  def devices_entry(chip: str) -> dict:
72
- """该型号在 raw/templates/*/devices.json 中的参数条目(用于核对导出产物)。"""
73
- for dj in sorted((CODE_ROOT / "raw" / "templates").glob("*/devices.json")):
72
+ """该型号在 templates/*/devices.json 中的参数条目(用于核对导出产物)。"""
73
+ for dj in sorted((CODE_ROOT / "templates").glob("*/devices.json")):
74
74
  for d in json.loads(dj.read_text(encoding="utf-8")).get("devices", []):
75
75
  if d.get("device", "").upper() == chip.upper():
76
76
  return d
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ht32-code-mcp"
7
- version = "0.2.6"
7
+ version = "0.3.0"
8
8
  description = "HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出"
9
9
  requires-python = ">=3.11"
10
10
  license = "MIT"
@@ -0,0 +1,181 @@
1
+ """export_project_bundle:v2 契约导出(recipe 物化 + 原子发布 + manifest)。
2
+
3
+ v2 变更(相对旧"复制模板 + 改少量字段"的实现):
4
+ - 入口支持显式 config_id / device+board / clock_profile / retarget_profile;
5
+ 旧 chip 参数走只读适配层(v1-adapter.json),保持旧默认并给弃用警告;
6
+ - 物化由 materializer.py 完成:先校验后 staging、XML 声明式改写(命中数约束)、
7
+ 内存一处裁决多处一致、manifest 四件套、staging 原子重命名、ZIP .part 原子发布;
8
+ - 响应字段:contract_version/config_id/recipe_revision/device/board/compiler/
9
+ profiles/project_relative_path/manifest_relative_path/artifact_sha256/
10
+ artifact_size/requires/warnings/validation。
11
+
12
+ 模板(templates/)是派生快照(replay_templates.py 可重建);唯一事实源是
13
+ .work/template-recipes/(schema + recipes + mapping + inventory)。
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import dataclasses
18
+ import shutil
19
+ import tempfile
20
+ import time
21
+ from pathlib import Path
22
+
23
+ from .materializer import (CONTRACT_VERSION, ContractError, Materializer, Resolved,
24
+ zip_tree_atomic)
25
+
26
+ DEFAULT_BUNDLE_TTL = 3600
27
+
28
+
29
+ @dataclasses.dataclass
30
+ class ExportResult:
31
+ """一次导出的结果(本地目录/云端 zip 共用)。"""
32
+
33
+ text: str
34
+ config_id: str
35
+ device: str
36
+ board: str
37
+ compiler: str
38
+ profiles: dict
39
+ project_relative_path: str
40
+ manifest_relative_path: str
41
+ artifact_sha256: str
42
+ artifact_size: int
43
+ requires: dict
44
+ warnings: list
45
+ validation: dict
46
+ out_dir: Path | None = None
47
+ zip_path: Path | None = None
48
+ download_url: str = ""
49
+ expires_at: float = 0.0
50
+
51
+
52
+ class ProjectExporter:
53
+ """v2 导出器:recipe 选择 -> 物化 -> 原子发布。"""
54
+
55
+ def __init__(self, code_root: Path) -> None:
56
+ self.root = Path(code_root)
57
+ recipes_dir = self.root / ".work" / "template-recipes" / "recipes"
58
+ self.materializer = Materializer(self.root, recipes_dir)
59
+
60
+ # ------------------------------------------------------------ 解析
61
+ def resolve(self, *, config_id: str = "", chip: str = "", device: str = "",
62
+ board: str = "", clock_profile: str = "",
63
+ retarget_profile: str = "") -> Resolved:
64
+ return self.materializer.resolve(
65
+ config_id=config_id, chip=chip, device=device,
66
+ board=board, clock_profile=clock_profile, retarget_profile=retarget_profile)
67
+
68
+ # ------------------------------------------------------------ 本地目录
69
+ def export_dir(self, *, out_dir: str, project_name: str = "", config_id: str = "",
70
+ chip: str = "", device: str = "", board: str = "",
71
+ clock_profile: str = "", retarget_profile: str = "",
72
+ include_host_tools: bool = False) -> ExportResult:
73
+ r = self.resolve(config_id=config_id, chip=chip, device=device, board=board,
74
+ clock_profile=clock_profile, retarget_profile=retarget_profile)
75
+ res = self.materializer.materialize(
76
+ r, out_dir, project_name=project_name, include_host_tools=include_host_tools)
77
+ return self._result(r, res, artifact_kind="directory")
78
+
79
+ # ------------------------------------------------------------ 云端 zip
80
+ def export_zip(self, *, bundle_dir: str, ttl_seconds: int = DEFAULT_BUNDLE_TTL,
81
+ project_name: str = "", config_id: str = "", chip: str = "",
82
+ device: str = "", board: str = "", clock_profile: str = "",
83
+ retarget_profile: str = "",
84
+ include_host_tools: bool = False) -> ExportResult:
85
+ r = self.resolve(config_id=config_id, chip=chip, device=device, board=board,
86
+ clock_profile=clock_profile, retarget_profile=retarget_profile)
87
+ bdir = Path(bundle_dir).expanduser().resolve()
88
+ bdir.mkdir(parents=True, exist_ok=True)
89
+ tmp = Path(tempfile.mkdtemp(prefix="ht32-bundle-", dir=str(bdir)))
90
+ try:
91
+ res = self.materializer.materialize(
92
+ r, tmp / "bundle", project_name=project_name,
93
+ include_host_tools=include_host_tools)
94
+ import secrets
95
+ token = secrets.token_urlsafe(24)
96
+ zip_path = bdir / f"{token}.zip"
97
+ size, digest = zip_tree_atomic(tmp / "bundle", zip_path)
98
+ result = self._result(r, res, artifact_kind="zip")
99
+ result.zip_path = zip_path
100
+ result.artifact_sha256 = digest
101
+ result.artifact_size = size
102
+ result.expires_at = time.time() + ttl_seconds
103
+ return result
104
+ finally:
105
+ if tmp.exists():
106
+ shutil.rmtree(tmp, ignore_errors=True)
107
+
108
+ # ------------------------------------------------------------ 结果文本
109
+ def _result(self, r: Resolved, res, *, artifact_kind: str) -> ExportResult:
110
+ cfg = r.config
111
+ lock = res.out_dir / "manifest" / "project-lock.json"
112
+ manifest_rel = "manifest/project-lock.json"
113
+ requires = {
114
+ "pack": r.recipe["pack"]["id"],
115
+ "pack_note": r.recipe["pack"]["device_macro_note"],
116
+ "keil": "MDK-ARM 5.x (AC5)",
117
+ }
118
+ validation = dict(cfg["validation"])
119
+ validation["static"] = "pass"
120
+ result = ExportResult(
121
+ text="", config_id=r.config_id, device=cfg["device"],
122
+ board=cfg["board"]["id"], compiler=cfg["compiler"]["id"],
123
+ profiles={"clock": r.clock_profile, "retarget": r.retarget_profile},
124
+ project_relative_path=res.project_file,
125
+ manifest_relative_path=manifest_rel,
126
+ artifact_sha256=res.sha256, artifact_size=res.bytes,
127
+ requires=requires, warnings=list(r.warnings), validation=validation,
128
+ out_dir=res.out_dir)
129
+ result.text = self._report(r, res, artifact_kind)
130
+ return result
131
+
132
+ def _report(self, r: Resolved, res, artifact_kind: str) -> str:
133
+ cfg = r.config
134
+ mem = cfg["memory"]
135
+ lines = [
136
+ f"自包含 Keil 工程已导出(v2 契约,{artifact_kind}):",
137
+ f"- contract_version: {CONTRACT_VERSION}",
138
+ f"- config_id: {r.config_id}"
139
+ + ("(非默认 profile 组合的派生 id)" if r.derived_config_id else ""),
140
+ f"- recipe_revision: {r.recipe['recipe_revision']}"
141
+ f"(materializer {r.recipe['materializer']['id']}"
142
+ f" r{r.recipe['materializer']['revision']})",
143
+ f"- device: {cfg['device']}|board: {cfg['board']['id']}"
144
+ f"|compiler: {cfg['compiler']['id']}",
145
+ f"- profiles: clock={r.clock_profile}, retarget={r.retarget_profile}",
146
+ f"- 工程文件: {res.project_file}",
147
+ f"- manifest: manifest/project-lock.json(另有 files/deviations/validation)",
148
+ f"- 内存(一处裁决): Flash {mem['flash'][0]['size']}"
149
+ f" @ {mem['flash'][0]['base']};RAM {mem['ram'][0]['size']}"
150
+ f" @ {mem['ram'][0]['base']}"
151
+ + (f";冲突记录 {len(mem['conflicts'])} 条" if mem["conflicts"] else ""),
152
+ f"- 文件数/字节: {res.files} / {res.bytes}",
153
+ f"- 树 SHA256: {res.sha256}",
154
+ f"- 前置: {r.recipe['pack']['device_macro_note']}",
155
+ ]
156
+ if r.retarget_profile == "retarget":
157
+ claims = cfg["retarget"]["uart_pin_claims"]
158
+ lines.append(f"- 串口占用: {claims[0] if claims else 'COM1'}")
159
+ if r.warnings:
160
+ lines.append("- 警告:")
161
+ lines.extend(f" - {w}" for w in r.warnings)
162
+ v = cfg["validation"]
163
+ lines.append(f"- validation: static=pass, compile={v['compile']}, "
164
+ f"gui={v['gui']}, hardware={v['hardware']}"
165
+ "(gui/hardware 未验证项以 not-tested 明示)")
166
+ lines.append("")
167
+ lines.append("后续:UV4 -b 基线编译(目标 0 Error 0 Warning);"
168
+ "换芯片请用显式 config_id 重新导出,不要手改 Device/Cpu/宏。")
169
+ return "\n".join(lines)
170
+
171
+
172
+ # 旧兼容入口(v1 chip 参数只读适配;调用方逐步迁移到 config_id)
173
+ def export_project_bundle_legacy(exporter: ProjectExporter, chip: str, out_dir: str,
174
+ project_name: str = "") -> str:
175
+ try:
176
+ result = exporter.export_dir(out_dir=out_dir, project_name=project_name, chip=chip)
177
+ except ContractError as e:
178
+ return f"导出失败:[{e.code}] {e.message}"
179
+ except OSError as e:
180
+ return f"导出失败(写盘/打包失败,检查目录权限与空间):{e}"
181
+ return result.text
@@ -166,14 +166,15 @@ class CodeIndex:
166
166
  raw_dir = self.root / "raw"
167
167
  if raw_dir.is_dir():
168
168
  data["fwlibs"] = [self._scan_fwlib(p) for p in sorted(raw_dir.iterdir())
169
- if p.is_dir() and p.name != "templates"]
170
- templates_dir = raw_dir / "templates"
171
- data["templates"] = [self._scan_template(p) for p in sorted(templates_dir.iterdir())
172
- if p.is_dir()] if templates_dir.is_dir() else []
169
+ if p.is_dir()]
173
170
  data["files"] = sorted(
174
171
  _rel(f.relative_to(self.root).as_posix())
175
172
  for f in raw_dir.rglob("*") if f.is_file()
176
173
  )
174
+ templates_dir = self.root / "templates"
175
+ if templates_dir.is_dir():
176
+ data["templates"] = [self._scan_template(p) for p in sorted(templates_dir.iterdir())
177
+ if p.is_dir()]
177
178
  data["knowledge_pages"] = self._scan_knowledge()
178
179
  fw = data["fwlibs"]
179
180
  data["stats"] = {