ht32-code-mcp 0.4.1__tar.gz → 0.4.2__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 (26) hide show
  1. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/PKG-INFO +1 -1
  2. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/pyproject.toml +1 -1
  3. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/src/ht32_code_mcp/materializer.py +126 -6
  4. ht32_code_mcp-0.4.2/tests/test_recipe_claims.py +92 -0
  5. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/tests/test_tools.py +49 -0
  6. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/uv.lock +1 -1
  7. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/.gitignore +0 -0
  8. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/.gitkeep +0 -0
  9. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/.pytest_base_final_run.log +0 -0
  10. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/DEPLOY.md +0 -0
  11. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/README.md +0 -0
  12. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/e2e_scenario_a.py +0 -0
  13. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/e2e_scenario_b.py +0 -0
  14. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/smoke_http.py +0 -0
  15. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/smoke_stdio.py +0 -0
  16. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/src/ht32_code_mcp/__init__.py +0 -0
  17. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/src/ht32_code_mcp/__main__.py +0 -0
  18. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/src/ht32_code_mcp/exporter.py +0 -0
  19. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/src/ht32_code_mcp/index.py +0 -0
  20. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/src/ht32_code_mcp/knowledge.py +0 -0
  21. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/src/ht32_code_mcp/release.py +0 -0
  22. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/src/ht32_code_mcp/server.py +0 -0
  23. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/src/ht32_code_mcp/usage.py +0 -0
  24. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/tests/conftest.py +0 -0
  25. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/tests/test_export_cloud_mode.py +0 -0
  26. {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.2}/tests/test_knowledge_grounding.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ht32-code-mcp
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出
5
5
  License-Expression: MIT
6
6
  Keywords: firmware,holtek,ht32,mcp,mcu
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ht32-code-mcp"
7
- version = "0.4.1"
7
+ version = "0.4.2"
8
8
  description = "HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出"
9
9
  requires-python = ">=3.11"
10
10
  license = "MIT"
@@ -95,6 +95,28 @@ def pack_release_version(pack_path: Path) -> str:
95
95
  return m.group(1).decode()
96
96
 
97
97
 
98
+ def pack_device_algorithm(pack_path: Path, device: str) -> str | None:
99
+ """DFP pdsc 中该 device 的首个 Flash 算法路径(如 `Flash/HT32F490x1_64.FLM`)。
100
+
101
+ 只从随包 DFP 取,不按型号猜:M4 同一子系列不同容量用的是不同 FLM
102
+ (F49041=_64、F49031=_32;F49153=_128、F49163=_256;F49365=_256、F49395=_1024)。
103
+ """
104
+ try:
105
+ with zipfile.ZipFile(pack_path) as z:
106
+ names = [n for n in z.namelist() if n.lower().endswith(".pdsc")]
107
+ if not names:
108
+ return None
109
+ data = z.read(names[0]).decode("utf-8", errors="replace")
110
+ except zipfile.BadZipFile:
111
+ return None
112
+ m = re.search(r'<device[^>]*Dname="' + re.escape(device) + r'"[^>]*>(.*?)</device>',
113
+ data, re.S)
114
+ if not m:
115
+ return None
116
+ alg = re.search(r'<algorithm[^>]*name="([^"]+\.FLM)"', m.group(1))
117
+ return alg.group(1) if alg else None
118
+
119
+
98
120
  # ------------------------------------------------------------------ recipe 装载
99
121
  def load_recipes(recipes_dir: Path) -> dict[str, dict]:
100
122
  recipes: dict[str, dict] = {}
@@ -752,12 +774,7 @@ class Materializer:
752
774
  # uvoptx(受控调试配置;M4 用官方侧车,f5 用净化后的用户侧车,l5/f1 不提供)
753
775
  uvoptx_src = self._controlled_uvoptx(r)
754
776
  if uvoptx_src is not None:
755
- data = uvoptx_src.read_bytes()
756
- data, n = re.subn(rb"(<TargetName>)[^<]*(</TargetName>)",
757
- lambda m: m.group(1) + stem.encode() + m.group(2), data, count=1)
758
- if n == 1:
759
- deviations.append(dict(kind="uvoptx-targetname", file=uvprojx.name,
760
- reason="受控 .uvoptx 的 TargetName 与工程名同步"))
777
+ data = self._render_uvoptx(r, uvprojx, uvoptx_src, pack, stem, deviations)
761
778
  target = proj_dir / f"{stem}.uvoptx"
762
779
  target.write_bytes(data)
763
780
  manifest_files.append(dict(
@@ -884,6 +901,23 @@ class Materializer:
884
901
  if f"<{tag}>" in text:
885
902
  text = rewrite_tag_value(text, tag, stem, what)
886
903
  edits.append(f"{tag} -> {stem}")
904
+ # e2) 声明式 uvprojx 选项编辑(如 M4 统一 -O2;命中数必须精确)
905
+ for e in cfg.get("project_edits", []):
906
+ tag = e["tag"]
907
+ old = f"<{tag}>{e['old_value']}</{tag}>"
908
+ new = f"<{tag}>{e['new_value']}</{tag}>"
909
+ hits = text.count(old)
910
+ if hits != e["expected_matches"]:
911
+ raise ContractError(
912
+ RECIPE_MISMATCH,
913
+ f"{what}: <{tag}>{e['old_value']}</{tag}> 预期命中 "
914
+ f"{e['expected_matches']} 次,实际 {hits}")
915
+ text = text.replace(old, new)
916
+ edits.append(f"{tag} {e['old_value']} -> {e['new_value']}")
917
+ deviations.append(dict(
918
+ kind="project-option-edit", file=f"{stem}.uvprojx", tag=tag,
919
+ old=e["old_value"], new=e["new_value"],
920
+ reason=e.get("reason") or "声明式项目选项编辑"))
887
921
  # f) PackID -> recipe 锁定版本(声明式)
888
922
  pack_id = r.recipe["pack"]["id"]
889
923
  if f"<PackID>{pack_id}</PackID>" not in text and "<PackID>" in text:
@@ -918,6 +952,92 @@ class Materializer:
918
952
  f"{r.series}-controlled.uvoptx"
919
953
  return asset if asset.is_file() else None
920
954
 
955
+ def _render_uvoptx(self, r: Resolved, uvprojx: Path, src: Path, pack: dict,
956
+ stem: str, deviations: list[dict]) -> bytes:
957
+ """把受控 .uvoptx 渲染成该 config 的调试侧车。
958
+
959
+ 受控资产只保留骨架与占位符;芯片相关值全部来自**已裁决的 recipe.memory +
960
+ 随包 DFP 的 Flash 算法**,不按型号猜:
961
+ - device / FLM:`pack_device_algorithm()`(M4 同子系列不同容量 FLM 不同);
962
+ - Flash 基址/容量:`memory.flash[0]`;
963
+ - SW-DP ID:M4=2BA01477,M0+/M3=0BC11477;
964
+ - ListingPath:与 uvprojx 同步(uVision 以 uvoptx 为准);
965
+ - CMSIS_AGDI 注册项含 `-FO15`(Reset and Run),与 M0+ 对齐。
966
+ """
967
+ cfg = r.config
968
+ what = cfg["config_id"]
969
+ device = cfg["device"]
970
+ flash = cfg["memory"]["flash"][0]
971
+ flash_base = int(flash["base"], 16)
972
+ flash_size = int(flash["size"], 16)
973
+ uv_text = uvprojx.read_text(encoding="utf-8", errors="replace")
974
+ m = re.search(r"<ListingPath>([^<]*)</ListingPath>", uv_text)
975
+ if not m:
976
+ raise ContractError(RECIPE_MISMATCH, f"{what}: uvprojx 缺 <ListingPath>")
977
+ listing = m.group(1)
978
+ m4 = r.series in ("ht32f490x1", "ht32f491x3", "ht32f493x5")
979
+ ram_alg = "1000"
980
+ if m4:
981
+ alg = pack_device_algorithm(pack["asset"], device)
982
+ if not alg:
983
+ raise ContractError(
984
+ RECIPE_MISMATCH,
985
+ f"{what}: 随包 DFP 未给出 {device} 的 Flash 算法(pdsc 无 algorithm)")
986
+ flm_path = alg.replace("/", "\\")
987
+ flm_base = Path(alg).stem
988
+ cmsis = (
989
+ '-X"Any" -UAny -O206 -S0 -C0 -P00 -N00("ARM CoreSight SW-DP") '
990
+ '-D00(2BA01477) -L00(0) -TO18 -TC10000000 -TP20 -TDS8007 -TDT0 -TDC1F '
991
+ '-TIEFFFFFFFF -TIP8 -FO15 -FD20000000 '
992
+ f"-FC{ram_alg} -FN1 -FF0{flm_base}.FLM "
993
+ f"-FS0{flash_base:X} -FL0{flash_size:X} "
994
+ f"-FP0($$Device:{device}${flm_path})")
995
+ ul2 = (
996
+ f"UL2CM3(-S0 -C0 -P0 -FD20000000 -FC{ram_alg} -FN1 -FF0{flm_base} "
997
+ f"-FS0{flash_base:X} -FL0{flash_size:X} "
998
+ f"-FP0($$Device:{device}${flm_path}))")
999
+ else:
1000
+ cmsis = (
1001
+ '-X"Any" -UAny -O206 -S0 -C0 -P00 -N00("ARM CoreSight SW-DP") '
1002
+ '-D00(0BC11477) -L00(0) -TO18 -TC1000000 -TP20 -TDS8007 -TDT0 -TDC1F '
1003
+ '-TIEFFFFFFFF -TIP8 -FO15 -FD20000000 '
1004
+ f"-FC{ram_alg} -FN2 -FF0HT32F.FLM "
1005
+ f"-FS0{flash_base:X} -FL0{flash_size:X} "
1006
+ f"-FP0($$Device:{device}$ARM\\Flash\\HT32F.FLM) "
1007
+ "-FF1HT32F_OPT.FLM -FS11FF00000 -FL11000 "
1008
+ f"-FP1($$Device:{device}$ARM\\Flash\\HT32F_OPT.FLM)")
1009
+ ul2 = (
1010
+ f"UL2CM3(-S0 -C0 -P0 -FD20000000 -FC{ram_alg} -FN2 -FF0HT32F "
1011
+ f"-FS0{flash_base:X} -FL0{flash_size:X} "
1012
+ "-FF1HT32F_OPT -FS11FF00000 -FL11000 "
1013
+ f"-FP0($$Device:{device}$ARM\\Flash\\HT32F.FLM) "
1014
+ f"-FP1($$Device:{device}$ARM\\Flash\\HT32F_OPT.FLM))")
1015
+ data = src.read_bytes()
1016
+ edits: list[str] = []
1017
+ for token, value in (
1018
+ (b"__HT32_LISTING__", listing),
1019
+ (b"__HT32_CMSIS_AGDI_NAME__", cmsis),
1020
+ (b"__HT32_UL2CM3_NAME__", ul2)):
1021
+ hits = data.count(token)
1022
+ if hits != 1:
1023
+ raise ContractError(
1024
+ RECIPE_MISMATCH,
1025
+ f"{what}: 受控 uvoptx 占位符 {token.decode()} 预期 1 次,实际 {hits}")
1026
+ data = data.replace(token, value.encode())
1027
+ edits.append(f"{token.decode()} -> {value[:60]}...")
1028
+ data, n = re.subn(rb"(<TargetName>)[^<]*(</TargetName>)",
1029
+ lambda mm: mm.group(1) + stem.encode() + mm.group(2),
1030
+ data, count=1)
1031
+ if n == 1:
1032
+ edits.append(f"TargetName -> {stem}")
1033
+ if b"__HT32_" in data:
1034
+ raise ContractError(RECIPE_MISMATCH, f"{what}: uvoptx 仍有未替换占位符")
1035
+ deviations.append(dict(
1036
+ kind="uvoptx-materialized", file=f"{stem}.uvoptx", edits=edits,
1037
+ reason="受控 uvoptx 按 device/FLM/memory/uvprojx ListingPath 参数化"
1038
+ "(含 CMSIS_AGDI -FO15 = Reset and Run)"))
1039
+ return data
1040
+
921
1041
  # ------------------------------------------------------------ manifest
922
1042
  def _write_manifests(self, r: Resolved, mdir: Path, stem: str, files: list[dict],
923
1043
  deviations: list[dict], notes: list[str], total_bytes: int,
@@ -0,0 +1,92 @@
1
+ """回归测试:retarget 串口声明(uart_pin_claims)的文案与真实头文件/宏一致。
2
+
3
+ 两个已实测的缺陷(都在 build_recipes.py 的 com1_claim() 里把宏名直接拼字符串):
4
+ 1. 版级头文件名:README 写 `per utilities/HT32_Board/ht32f50241-sk.h`,实际文件是
5
+ `ht32f50241_sk.h`(板名 id 用连字符、头文件名用下划线)。从 recipe 数据侧兜底:
6
+ 每条 claim 里的头文件名都必须能在官方包 `utilities/HT32_Board/` 下找到。
7
+ 2. 引脚宏粘连:README 写 `TX=GPIO_PAAFIO_PIN_4`,实际是「端口宏 GPIO_PA + 引脚宏
8
+ AFIO_PIN_4」两个独立宏,应为 `TX=GPIO_PA/AFIO_PIN_4`。
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re
14
+ from pathlib import Path
15
+
16
+ REPO = Path(__file__).resolve().parents[3] # tests -> mcp-server -> .work -> ht32-code
17
+ RECIPES = REPO / ".work" / "template-recipes" / "recipes"
18
+ CLAIM_HEADER = re.compile(r"utilities/HT32_Board/([^)\s]+\.h)")
19
+ # 引脚 = 端口宏 GPIO_Px + 引脚宏 AFIO_PIN_n,中间必须有一个 '/'
20
+ CLAIM_PIN = re.compile(r"GPIO_P[A-H]?/AFIO_PIN_\d+")
21
+ CLAIM_PIN_VALUE = re.compile(r"(?:TX|RX)=([^\s,)]+)")
22
+
23
+
24
+ def _recipes() -> list[dict]:
25
+ return [json.loads(p.read_text(encoding="utf-8")) for p in sorted(RECIPES.glob("*.json"))]
26
+
27
+
28
+ def test_claim_board_headers_exist_in_official_package():
29
+ missing: list[str] = []
30
+ checked = 0
31
+ for recipe in _recipes():
32
+ for cfg in recipe["configs"]:
33
+ for claim in (cfg.get("retarget") or {}).get("uart_pin_claims") or []:
34
+ m = CLAIM_HEADER.search(claim)
35
+ if not m:
36
+ continue
37
+ checked += 1
38
+ header = REPO / "raw" / cfg["origin"]["package"] / "utilities" / "HT32_Board" / m.group(1)
39
+ if not header.is_file():
40
+ missing.append(f"{cfg['config_id']}: {m.group(1)}")
41
+ assert checked > 0, "recipe 里没有可校验的板级头文件声明"
42
+ assert not missing, "claim 引用了不存在的板级头文件:\n" + "\n".join(missing)
43
+
44
+
45
+ def test_claim_board_headers_use_real_file_names_not_board_ids():
46
+ """板名 id(连字符)不能直接当文件名;下划线才是官方文件名的分隔符。"""
47
+ wrong: list[str] = []
48
+ for recipe in _recipes():
49
+ for cfg in recipe["configs"]:
50
+ for claim in (cfg.get("retarget") or {}).get("uart_pin_claims") or []:
51
+ m = CLAIM_HEADER.search(claim)
52
+ if m and ("-sk.h" in m.group(1) or "-dvb.h" in m.group(1)):
53
+ wrong.append(f"{cfg['config_id']}: {m.group(1)}")
54
+ assert not wrong, "claim 头文件名仍用板名 id 的连字符:\n" + "\n".join(wrong)
55
+
56
+
57
+ def test_claim_pins_use_port_macro_slash_pin_macro():
58
+ """7 份 recipe 的 TX=/RX= 引脚都必须是 GPIO_Px/AFIO_PIN_n 形态。
59
+
60
+ 端口宏(GPIO_PA)与引脚宏(AFIO_PIN_4)是两个独立宏;拼成 GPIO_PAAFIO_PIN_4
61
+ 会被用户当成宏名照抄,编译不过。
62
+ """
63
+ bad: list[str] = []
64
+ checked = 0
65
+ for recipe in _recipes():
66
+ for cfg in recipe["configs"]:
67
+ for claim in (cfg.get("retarget") or {}).get("uart_pin_claims") or []:
68
+ for m in CLAIM_PIN_VALUE.finditer(claim):
69
+ checked += 1
70
+ if not CLAIM_PIN.fullmatch(m.group(1)):
71
+ bad.append(f"{cfg['config_id']}: {m.group(0)} <- {claim}")
72
+ assert checked >= 80, f"recipe 里可校验的 TX/RX 引脚过少({checked}),疑似数据缺失"
73
+ assert not bad, ("claim 引脚宏形态不对(应为 GPIO_Px/AFIO_PIN_n,不能写成 "
74
+ "GPIO_PAAFIO_PIN_n):\n" + "\n".join(bad))
75
+
76
+
77
+ def test_exported_readme_points_at_real_board_header(tmp_path):
78
+ """导出工程 README 的串口占用应指向真实板级头文件(下划线)、引脚宏用 '/' 分隔。"""
79
+ from ht32_code_mcp import server as srv
80
+
81
+ out = tmp_path / "bundle_f50241"
82
+ text = srv.export_project_bundle("HT32F50241", str(out))
83
+ assert "导出失败" not in text, text
84
+ readme = (out / "README.md").read_text(encoding="utf-8")
85
+ assert "ht32f50241_sk.h" in readme, readme
86
+ assert "ht32f50241-sk.h" not in readme, readme
87
+ # README「占用」行必须给出 GPIO_PA/AFIO_PIN_4(板级头:TX=PA4 / RX=PA5)
88
+ claim_line = next((ln for ln in readme.splitlines() if "占用" in ln), "")
89
+ assert "TX=GPIO_PA/AFIO_PIN_4" in claim_line, claim_line
90
+ assert "RX=GPIO_PA/AFIO_PIN_5" in claim_line, claim_line
91
+ assert "GPIO_PAAFIO" not in readme, readme
92
+
@@ -478,6 +478,55 @@ def test_export_target_name_custom(tmp_path):
478
478
  assert (out / "README.md").read_text(encoding="utf-8").startswith("# F52352_UART_Printf")
479
479
 
480
480
 
481
+ @pytest.mark.parametrize("config_id,device,flm", [
482
+ ("ht32f49041-20tssop.ht32f49041-sk.ac5.hick-safe.retarget",
483
+ "HT32F49041_20TSSOP", "HT32F490x1_64"),
484
+ ("ht32f49153-48lqfp.ht32f49163-sk.ac5.hick-safe.retarget",
485
+ "HT32F49153_48LQFP", "HT32F491x3_128"),
486
+ ("ht32f49365-64lqfp.ht32f49395-sk.ac5.hick-safe.retarget",
487
+ "HT32F49365_64LQFP", "HT32F493x5_256"),
488
+ ])
489
+ def test_export_m4_debug_options_and_optimization(tmp_path, config_id, device, flm):
490
+ """M4:-O2、CMSIS_AGDI(-FO15)、FLM/device 按芯片参数化、ListingPath 同步。"""
491
+ out = tmp_path / config_id.split(".")[0]
492
+ srv.exporter.export_dir(out_dir=str(out), project_name="verify_proj", config_id=config_id)
493
+ uv = next((out / "project" / "MDK_ARMv5").glob("*.uvprojx"))
494
+ text = uv.read_text(encoding="utf-8")
495
+ assert "<Optim>3</Optim>" in text
496
+ listing = re.search(r"<ListingPath>([^<]*)</ListingPath>", text).group(1)
497
+ side = uv.with_suffix(".uvoptx")
498
+ side_text = side.read_text(encoding="utf-8")
499
+ assert "<Key>CMSIS_AGDI</Key>" in side_text
500
+ assert "<Key>UL2CM3</Key>" in side_text
501
+ assert "-FO15" in side_text
502
+ assert re.search(r"<ListingPath>" + re.escape(listing) + "</ListingPath>", side_text)
503
+ assert f"$$Device:{device}$" in side_text
504
+ assert flm in side_text
505
+ dev = json.loads((out / "manifest" / "deviations.json").read_text(encoding="utf-8"))
506
+ kinds = [d["kind"] for d in dev["deviations"]]
507
+ assert "project-option-edit" in kinds
508
+ assert "uvoptx-materialized" in kinds
509
+
510
+
511
+ def test_export_m0_debug_sidecar_is_parameterized(tmp_path):
512
+ """M0+:uvoptx 的 device/Flash 容量与 uvprojx ListingPath 同步(不再固定 F52352)。"""
513
+ out = tmp_path / "f50030"
514
+ srv.exporter.export_dir(
515
+ out_dir=str(out), project_name="verify_proj",
516
+ config_id="ht32f50030.ht32f50030-sk.ac5.hsi-safe.retarget")
517
+ uv = next((out / "project" / "MDK_ARMv5").glob("*.uvprojx"))
518
+ text = uv.read_text(encoding="utf-8")
519
+ listing = re.search(r"<ListingPath>([^<]*)</ListingPath>", text).group(1)
520
+ output_dir = re.search(r"<OutputDirectory>([^<]*)</OutputDirectory>", text).group(1)
521
+ # obj 与 list 是同一芯片目录下的两个并列子目录;list 不得再落回工程根 .\List\
522
+ assert "HT32\\50030\\Obj\\" in output_dir
523
+ assert "HT32\\50030\\List\\" in listing
524
+ side_text = uv.with_suffix(".uvoptx").read_text(encoding="utf-8")
525
+ assert re.search(r"<ListingPath>" + re.escape(listing) + "</ListingPath>", side_text)
526
+ assert "$$Device:HT32F50030$" in side_text
527
+ assert "$$Device:HT32F52352$" not in side_text
528
+
529
+
481
530
  def test_export_unknown_chip(tmp_path):
482
531
  result = srv.export_project_bundle("HT32F99999", str(tmp_path / "nope"))
483
532
  assert "UNKNOWN_DEVICE" in result and "不在契约" in result
@@ -440,7 +440,7 @@ wheels = [
440
440
 
441
441
  [[package]]
442
442
  name = "ht32-code-mcp"
443
- version = "0.4.1"
443
+ version = "0.4.2"
444
444
  source = { editable = "." }
445
445
  dependencies = [
446
446
  { name = "fastmcp" },
File without changes
File without changes
File without changes
File without changes