ht32-code-mcp 0.3.1__tar.gz → 0.3.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 (25) hide show
  1. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/PKG-INFO +1 -1
  2. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/pyproject.toml +1 -1
  3. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/src/ht32_code_mcp/materializer.py +14 -9
  4. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/src/ht32_code_mcp/server.py +6 -0
  5. ht32_code_mcp-0.3.2/tests/test_export_cloud_mode.py +47 -0
  6. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/uv.lock +1 -1
  7. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/.gitignore +0 -0
  8. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/.gitkeep +0 -0
  9. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/.pytest_base_final_run.log +0 -0
  10. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/DEPLOY.md +0 -0
  11. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/README.md +0 -0
  12. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/e2e_scenario_a.py +0 -0
  13. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/e2e_scenario_b.py +0 -0
  14. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/smoke_http.py +0 -0
  15. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/smoke_stdio.py +0 -0
  16. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/src/ht32_code_mcp/__init__.py +0 -0
  17. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/src/ht32_code_mcp/__main__.py +0 -0
  18. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/src/ht32_code_mcp/exporter.py +0 -0
  19. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/src/ht32_code_mcp/index.py +0 -0
  20. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/src/ht32_code_mcp/knowledge.py +0 -0
  21. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/src/ht32_code_mcp/release.py +0 -0
  22. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/src/ht32_code_mcp/usage.py +0 -0
  23. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/tests/conftest.py +0 -0
  24. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/tests/test_knowledge_grounding.py +0 -0
  25. {ht32_code_mcp-0.3.1 → ht32_code_mcp-0.3.2}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ht32-code-mcp
3
- Version: 0.3.1
3
+ Version: 0.3.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.3.1"
7
+ version = "0.3.2"
8
8
  description = "HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出"
9
9
  requires-python = ">=3.11"
10
10
  license = "MIT"
@@ -20,6 +20,7 @@ import os
20
20
  import re
21
21
  import shutil
22
22
  import tempfile
23
+ import threading
23
24
  import time
24
25
  import zipfile
25
26
  from pathlib import Path
@@ -381,6 +382,10 @@ class Materializer:
381
382
  self.raw = self.root / "raw"
382
383
  self.recipes = load_recipes(recipes_dir)
383
384
  self.recipes["__dir__"] = recipes_dir # resolve() 需要定位 adapter
385
+ # 单次调用的中间状态必须放 thread-local:物化器是进程级单例,
386
+ # 并发导出(MCP 工具在线程池里执行)会互相覆盖 _stage / _project_rel,
387
+ # 导致 "…is not in the subpath of…" 之类的相对路径串台错误。
388
+ self._local = threading.local()
384
389
 
385
390
  def resolve(self, *, config_id: str = "", chip: str = "", device: str = "",
386
391
  board: str = "", variant: str = "", clock_profile: str = "",
@@ -434,7 +439,7 @@ class Materializer:
434
439
  target.parent.mkdir(parents=True, exist_ok=True)
435
440
  shutil.copyfile(p, target)
436
441
  manifest_files.append(dict(
437
- path=(dst.relative_to(self._stage) / rel).as_posix(), bytes=p.stat().st_size,
442
+ path=(dst.relative_to(self._local.stage) / rel).as_posix(), bytes=p.stat().st_size,
438
443
  sha256=sha256_file(p), classification="official",
439
444
  origin=f"{pkg_name}/{src.relative_to(self.raw / pkg_name).as_posix()}/{rel}"))
440
445
 
@@ -456,7 +461,7 @@ class Materializer:
456
461
  # staging:与目标同文件系统的临时目录(保证原子重命名)
457
462
  out.parent.mkdir(parents=True, exist_ok=True)
458
463
  stage = Path(tempfile.mkdtemp(prefix=f".ht32-stage-{stem}-", dir=str(out.parent)))
459
- self._stage = stage
464
+ self._local.stage = stage
460
465
  manifest_files: list[dict] = []
461
466
  deviations: list[dict] = []
462
467
  try:
@@ -497,7 +502,7 @@ class Materializer:
497
502
  shutil.rmtree(stage, ignore_errors=True)
498
503
  except BaseException: # noqa: BLE001 - best-effort cleanup
499
504
  pass
500
- return MaterialResult(out_dir=out, project_file=self._project_rel,
505
+ return MaterialResult(out_dir=out, project_file=self._local.project_rel,
501
506
  files=len(manifest_files), bytes=total_bytes,
502
507
  sha256=tree_hash, manifest_dir="manifest",
503
508
  warnings=r.warnings, deviations=deviations)
@@ -509,7 +514,7 @@ class Materializer:
509
514
  cfg = r.config
510
515
  origin = cfg["origin"]
511
516
  pkg = self.raw / origin["package"]
512
- self._stage = stage
517
+ self._local.stage = stage
513
518
 
514
519
  # 1) library:完整官方树(complete-official-tree)
515
520
  lib_src = pkg / ("libraries" if kernel == "m4" else "library")
@@ -618,9 +623,9 @@ class Materializer:
618
623
  proj_dir = stage / "project" / "MDK_ARMv5"
619
624
  proj_dir.mkdir(parents=True, exist_ok=True)
620
625
  uvprojx = self._materialize_uvprojx(r, proj_dir, stem, kernel, board_dir, deviations)
621
- self._project_rel = uvprojx.relative_to(stage).as_posix()
626
+ self._local.project_rel = uvprojx.relative_to(stage).as_posix()
622
627
  uv_data = uvprojx.read_bytes()
623
- manifest_files.append(dict(path=self._project_rel, bytes=len(uv_data),
628
+ manifest_files.append(dict(path=self._local.project_rel, bytes=len(uv_data),
624
629
  sha256=sha256_bytes(uv_data),
625
630
  classification="materialized-project",
626
631
  origin=f"{origin['package']}/{origin['project']}"))
@@ -831,7 +836,7 @@ class Materializer:
831
836
  (mdir / "validation.json").write_text(json.dumps({
832
837
  "layers": validation,
833
838
  "gate_rule": "gui/hardware 未验证项以 not-tested 明示,不得宣称已验证",
834
- "compile_command": f'UV4 -b "{self._project_rel}" -j0 -o build.log',
839
+ "compile_command": f'UV4 -b "{self._local.project_rel}" -j0 -o build.log',
835
840
  }, ensure_ascii=False, indent=1), encoding="utf-8")
836
841
 
837
842
  def _readme(self, r: Resolved, stem: str) -> str:
@@ -855,8 +860,8 @@ class Materializer:
855
860
  "",
856
861
  "## 打开与编译",
857
862
  "",
858
- f"- 打开 `{self._project_rel}`(Target:`{stem}`)",
859
- f"- 命令行基线编译:`UV4 -b \"{self._project_rel}\" -j0 -o build.log`",
863
+ f"- 打开 `{self._local.project_rel}`(Target:`{stem}`)",
864
+ f"- 命令行基线编译:`UV4 -b \"{self._local.project_rel}\" -j0 -o build.log`",
860
865
  ]
861
866
  if r.series == "ht32f5":
862
867
  lines.append("- 调试:工程自带受控 `.uvoptx`(CMSIS-DAP + Reset and Run 配置;"
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  import os
9
9
  import sys
10
+ import time
10
11
  from importlib.metadata import PackageNotFoundError, version as pkg_version
11
12
  from pathlib import Path
12
13
  from typing import Literal
@@ -43,6 +44,11 @@ BUNDLE_BASE_URL = os.environ.get("BUNDLE_BASE_URL", "").strip()
43
44
  BUNDLE_TTL = _env_int("BUNDLE_TTL_SECONDS", DEFAULT_BUNDLE_TTL)
44
45
  CLOUD_BUNDLE = bool(BUNDLE_DIR and BUNDLE_BASE_URL)
45
46
 
47
+
48
+ def _fmt_time(ts: float) -> str:
49
+ """epoch 秒 → 本地时间字符串(用于云端 zip 下载的有效期提示)。"""
50
+ return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts))
51
+
46
52
  # 反馈与用量日志:FEEDBACK_ROOT(宿主机挂载的可写卷,与 wiki 同一套约定)
47
53
  # 下按服务名分子目录,本服务写 <FEEDBACK_ROOT>/code/。
48
54
  # 为空则整体关闭——本地开发默认不落盘。注意:日志含 query 原文,勿提交进仓库。
@@ -0,0 +1,47 @@
1
+ """回归测试:线上实测暴露的两个导出缺陷。
2
+
3
+ 1) 云端 zip 模式下响应格式化用了未定义的 `_fmt_time` → 线上 `export_project_bundle`
4
+ 100% 失败(本地模式与直接调 `exporter.export_zip` 都覆盖不到这行)。
5
+ 2) 物化器的单次调用状态(`_stage` / `_project_rel`)曾挂在进程级单例实例上,
6
+ 并发导出互相覆盖 → "…is not in the subpath of…" 串台错误。
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import concurrent.futures
11
+
12
+ from ht32_code_mcp import server as srv
13
+
14
+ CFG_F52352 = "ht32f52352.ht32f52352-sk.ac5.hsi-safe.retarget"
15
+ CFG_F50241 = "ht32f50241.ht32f50241-sk.ac5.hsi-safe.retarget"
16
+ CFG_L52241 = "ht32l52241.ht32l52241-sk.ac5.hsi-safe.retarget"
17
+
18
+
19
+ def test_cloud_zip_response_formats_expiry(tmp_path, monkeypatch):
20
+ """云端模式必须能给出下载 URL 与有效期(不得因未定义的 helper 直接抛 NameError)。"""
21
+ monkeypatch.setattr(srv, "CLOUD_BUNDLE", True)
22
+ monkeypatch.setattr(srv, "BUNDLE_DIR", str(tmp_path / "bundles"))
23
+ monkeypatch.setattr(srv, "BUNDLE_BASE_URL", "http://example.test/ht32-bundles")
24
+ text = srv.export_project_bundle(config_id=CFG_F52352, project_name="CloudMode")
25
+ assert "导出失败" not in text, text
26
+ assert "下载 URL" in text and "http://example.test/ht32-bundles/" in text
27
+ assert "有效期" in text
28
+ assert list(tmp_path.joinpath("bundles").glob("*.zip")), "未生成 zip 产物"
29
+
30
+
31
+ def test_concurrent_exports_do_not_cross_talk(tmp_path):
32
+ """并发导出必须各自落到自己的目录(不得因共享实例状态串台)。"""
33
+ jobs = [(CFG_F50241, "ConcA"), (CFG_L52241, "ConcB")]
34
+
35
+ def run(item):
36
+ idx, (cid, name) = item
37
+ return srv.export_project_bundle(
38
+ config_id=cid, project_name=name, out_dir=str(tmp_path / f"out{idx}"))
39
+
40
+ with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
41
+ texts = list(pool.map(run, enumerate(jobs)))
42
+
43
+ for text, (_, name) in zip(texts, jobs):
44
+ assert "导出失败" not in text, text
45
+ for idx, (_, name) in enumerate(jobs):
46
+ proj = tmp_path / f"out{idx}" / "project" / "MDK_ARMv5" / f"{name}.uvprojx"
47
+ assert proj.is_file(), f"并发导出产物缺失:{proj}"
@@ -440,7 +440,7 @@ wheels = [
440
440
 
441
441
  [[package]]
442
442
  name = "ht32-code-mcp"
443
- version = "0.3.0"
443
+ version = "0.3.2"
444
444
  source = { editable = "." }
445
445
  dependencies = [
446
446
  { name = "fastmcp" },
File without changes
File without changes
File without changes
File without changes