ht32-code-mcp 0.1.0__py3-none-any.whl
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.
- ht32_code_mcp/__init__.py +3 -0
- ht32_code_mcp/__main__.py +4 -0
- ht32_code_mcp/exporter.py +166 -0
- ht32_code_mcp/index.py +246 -0
- ht32_code_mcp/knowledge.py +100 -0
- ht32_code_mcp/server.py +327 -0
- ht32_code_mcp-0.1.0.dist-info/METADATA +15 -0
- ht32_code_mcp-0.1.0.dist-info/RECORD +10 -0
- ht32_code_mcp-0.1.0.dist-info/WHEEL +4 -0
- ht32_code_mcp-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""export_project_bundle:从模板 + 官方库源码组装自包含可编译 Keil 工程到本地目录。
|
|
2
|
+
|
|
3
|
+
本阶段(P0-2)不做 token 下载 URL,云端阶段再扩展。
|
|
4
|
+
参数化依据:raw/templates/ht32f5-keil/devices.json(device/cdef/adef/sys/startup)。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
_REDIRECT_RE = re.compile(r"#ifdef\s+(USE_\w+)\s*\n\s*#define\s+(USE_\w+)")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ExportError(Exception):
|
|
16
|
+
"""导出失败(型号未知 / 源文件缺失 / 目标目录非空等)。"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _read_text(path: Path) -> str:
|
|
20
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ProjectExporter:
|
|
24
|
+
def __init__(self, code_root: Path) -> None:
|
|
25
|
+
self.root = Path(code_root)
|
|
26
|
+
self.template_dir = self.root / "raw" / "templates" / "ht32f5-keil"
|
|
27
|
+
self.fwlib_dir = self.root / "raw" / "HT32_STD_5xxxx_FWLib_V1.21.1"
|
|
28
|
+
|
|
29
|
+
# ------------------------------------------------------------ 主流程
|
|
30
|
+
def export(self, chip: str, out_dir: str) -> str:
|
|
31
|
+
if not self.template_dir.is_dir():
|
|
32
|
+
raise ExportError(f"模板目录不存在:{self.template_dir}")
|
|
33
|
+
device, entry = self._resolve_device(chip)
|
|
34
|
+
|
|
35
|
+
# 1. 预检目标目录
|
|
36
|
+
out = Path(out_dir).expanduser().resolve()
|
|
37
|
+
if out.exists() and any(out.iterdir()):
|
|
38
|
+
raise ExportError(f"目标目录非空,拒绝覆盖:{out}(请指定新目录或先清空)")
|
|
39
|
+
|
|
40
|
+
# 2. 预检官方源文件(先验证后落盘,失败不产生半成品)
|
|
41
|
+
sys_c = self.fwlib_dir / "library" / "Device" / "Holtek" / "HT32F5xxxx" / "Source" / entry["sys"]
|
|
42
|
+
sys_h = sys_c.parent.parent / "Include" / (sys_c.stem + ".h")
|
|
43
|
+
startup = (self.fwlib_dir / "library" / "Device" / "Holtek" / "HT32F5xxxx"
|
|
44
|
+
/ "Source" / "ARM" / entry["startup"])
|
|
45
|
+
libcfg_name = self._resolve_libcfg(entry["cdef"])
|
|
46
|
+
libcfg = (self.fwlib_dir / "library" / "HT32F5xxxx_Driver" / "inc" / libcfg_name
|
|
47
|
+
if libcfg_name else None)
|
|
48
|
+
missing = [str(p) for p in (sys_c, sys_h, startup, libcfg) if p is not None and not p.is_file()]
|
|
49
|
+
if missing:
|
|
50
|
+
raise ExportError("官方库缺少所需源文件:\n- " + "\n- ".join(missing))
|
|
51
|
+
|
|
52
|
+
# 3. 复制模板树
|
|
53
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
shutil.copytree(self.template_dir, out, dirs_exist_ok=True)
|
|
55
|
+
|
|
56
|
+
# 4. 参数化 uvprojx
|
|
57
|
+
uvprojx = out / "MDK-ARM" / "ht32f5-template.uvprojx"
|
|
58
|
+
old_sys_stem = "system_ht32f5xxxx_01"
|
|
59
|
+
old_startup_stem = "startup_ht32f5xxxx_01"
|
|
60
|
+
text = _read_text(uvprojx)
|
|
61
|
+
text = re.sub(r"(<Device>)[^<]*(</Device>)", rf"\g<1>{device}\g<2>", text, count=1)
|
|
62
|
+
text = _replace_define(text, "USE_HT32_DRIVER", entry["cdef"])
|
|
63
|
+
text = _replace_define(text, "USE_HT32_CHIP", entry["adef"])
|
|
64
|
+
text = text.replace(old_sys_stem, sys_c.stem).replace(old_startup_stem, startup.stem)
|
|
65
|
+
with open(uvprojx, "w", encoding="utf-8", newline="") as f:
|
|
66
|
+
f.write(text)
|
|
67
|
+
|
|
68
|
+
# 5. 替换设备相关文件
|
|
69
|
+
replaced: list[str] = []
|
|
70
|
+
for old in out.glob("CMSIS/system_ht32f5xxxx_*.c"):
|
|
71
|
+
old.unlink()
|
|
72
|
+
for old in out.glob("CMSIS/system_ht32f5xxxx_*.h"):
|
|
73
|
+
old.unlink()
|
|
74
|
+
shutil.copyfile(sys_c, out / "CMSIS" / sys_c.name)
|
|
75
|
+
shutil.copyfile(sys_h, out / "CMSIS" / sys_h.name)
|
|
76
|
+
replaced += [f"CMSIS/{sys_c.name}", f"CMSIS/{sys_h.name}"]
|
|
77
|
+
|
|
78
|
+
for old in out.glob("Startup/startup_*.s"):
|
|
79
|
+
old.unlink()
|
|
80
|
+
shutil.copyfile(startup, out / "Startup" / startup.name)
|
|
81
|
+
replaced.append(f"Startup/{startup.name}")
|
|
82
|
+
|
|
83
|
+
if libcfg is not None:
|
|
84
|
+
for old in out.glob("Drivers/*_libcfg.h"):
|
|
85
|
+
old.unlink()
|
|
86
|
+
shutil.copyfile(libcfg, out / "Drivers" / libcfg.name)
|
|
87
|
+
replaced.append(f"Drivers/{libcfg.name}")
|
|
88
|
+
|
|
89
|
+
return self._report(out, device, entry, replaced, libcfg_name)
|
|
90
|
+
|
|
91
|
+
# ------------------------------------------------------------ 辅助
|
|
92
|
+
def _resolve_device(self, chip: str) -> tuple[str, dict]:
|
|
93
|
+
import json
|
|
94
|
+
dj = self.template_dir / "devices.json"
|
|
95
|
+
if not dj.is_file():
|
|
96
|
+
raise ExportError(f"模板型号参数表缺失:{dj}")
|
|
97
|
+
meta = json.loads(dj.read_text(encoding="utf-8"))
|
|
98
|
+
want = chip.strip().upper()
|
|
99
|
+
for d in meta.get("devices", []):
|
|
100
|
+
if d.get("device", "").upper() == want:
|
|
101
|
+
return d["device"], d # 重名取首条(官方板级变体)
|
|
102
|
+
# 宽松匹配:型号主体包含(HT32F52352 含于 HT32F52352_SK 场景已由上面精确处理)
|
|
103
|
+
for d in meta.get("devices", []):
|
|
104
|
+
if want in d.get("device", "").upper():
|
|
105
|
+
return d["device"], d
|
|
106
|
+
names = [d.get("device", "") for d in meta.get("devices", [])][:20]
|
|
107
|
+
raise ExportError(
|
|
108
|
+
f"型号 '{chip}' 不在模板参数表(共 {meta.get('count', '?')} 个型号)。\n"
|
|
109
|
+
f"可用型号(前 20):{', '.join(names)}…\n"
|
|
110
|
+
"完整清单:get_file('raw/templates/ht32f5-keil/devices.json') 或 get_project_manifest()"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def _resolve_libcfg(self, cdef: str) -> str | None:
|
|
114
|
+
"""从 cdef 宏解析 libcfg 文件名,沿 ht32_config.h 重定向链(USE_HT50F32002 → USE_HT32F50220_30)。"""
|
|
115
|
+
inc_dir = self.fwlib_dir / "library" / "HT32F5xxxx_Driver" / "inc"
|
|
116
|
+
config = (self.template_dir / "CMSIS" / "ht32_config.h")
|
|
117
|
+
redirects: dict[str, str] = {}
|
|
118
|
+
if config.is_file():
|
|
119
|
+
redirects = {m.group(1): m.group(2)
|
|
120
|
+
for m in _REDIRECT_RE.finditer(_read_text(config))}
|
|
121
|
+
for token in [t.strip() for t in cdef.split(",")]:
|
|
122
|
+
if not token.startswith("USE_"):
|
|
123
|
+
continue
|
|
124
|
+
cur, seen = token, set()
|
|
125
|
+
while cur and cur not in seen:
|
|
126
|
+
seen.add(cur)
|
|
127
|
+
candidate = inc_dir / (cur[4:].lower() + "_libcfg.h")
|
|
128
|
+
if candidate.is_file():
|
|
129
|
+
return candidate.name
|
|
130
|
+
cur = redirects.get(cur, "")
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
def _report(self, out: Path, device: str, entry: dict,
|
|
134
|
+
replaced: list[str], libcfg_name: str | None) -> str:
|
|
135
|
+
n_files = sum(1 for p in out.rglob("*") if p.is_file())
|
|
136
|
+
libcfg_note = (f"Drivers/{libcfg_name}" if libcfg_name
|
|
137
|
+
else "(未解析出 libcfg,保留模板默认,请人工核对)")
|
|
138
|
+
lines = [
|
|
139
|
+
f"自包含 Keil 工程已导出:{out}",
|
|
140
|
+
f"目标芯片:{device}({entry.get('sys')} / {entry.get('startup')})",
|
|
141
|
+
f"C 宏(Cads):{entry['cdef']}",
|
|
142
|
+
f"汇编宏(Aads):{entry['adef']}",
|
|
143
|
+
f"libcfg:{libcfg_note}",
|
|
144
|
+
f"工程文件:MDK-ARM/ht32f5-template.uvprojx(已按上述宏参数化)",
|
|
145
|
+
f"文件总数:{n_files}",
|
|
146
|
+
"",
|
|
147
|
+
"按目标芯片替换/注入的文件:",
|
|
148
|
+
*[f"- {r}" for r in replaced],
|
|
149
|
+
"",
|
|
150
|
+
"目录结构:Application(main/it/conf)、CMSIS(设备头+system)、"
|
|
151
|
+
"Drivers(最小驱动集+libcfg)、Startup、MDK-ARM(工程文件)、README.md、devices.json",
|
|
152
|
+
"",
|
|
153
|
+
"后续步骤:",
|
|
154
|
+
"1. Keil MDK 打开 MDK-ARM/ht32f5-template.uvprojx(需 Holtek HT32_DFP pack ≥1.0.67)",
|
|
155
|
+
"2. 如需精确 IROM/IRAM 内存布局,在 Options → Device 重新选择该型号由 DFP 刷新",
|
|
156
|
+
"3. 基线编译验证(P0-4.2 将用 UV4 命令行复核 0 Error 0 Warning)",
|
|
157
|
+
]
|
|
158
|
+
return "\n".join(lines)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _replace_define(text: str, anchor: str, new_value: str) -> str:
|
|
162
|
+
"""替换 uvprojx 中包含 anchor 的 <Define> 行内容(Cads/Aads 各一条)。"""
|
|
163
|
+
pattern = re.compile(r"(<Define>)[^<]*" + re.escape(anchor) + r"[^<]*(</Define>)")
|
|
164
|
+
if not pattern.search(text):
|
|
165
|
+
raise ExportError(f"uvprojx 中未找到含 {anchor} 的 <Define>,模板结构可能已变化")
|
|
166
|
+
return pattern.sub(rf"\g<1>{new_value}\g<2>", text, count=1)
|
ht32_code_mcp/index.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""预构建 JSON 索引:扫描 raw/ 与知识页,生成库模块/例程/文件路径清单。
|
|
2
|
+
|
|
3
|
+
生成产物:.work/mcp-server/index/code_index.json(gitignore,可随时重建)。
|
|
4
|
+
生成入口:.work/build_index.py(薄 CLI);server 启动时若索引缺失会自动构建。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
# 驱动源码文件名 → 库模块名的系列前缀(ht32f5xxxx_gpio.c → gpio)
|
|
15
|
+
_MODULE_PREFIX_RE = re.compile(
|
|
16
|
+
r"^(?:ht32f5xxxx|ht32f652xx|ht32f65xxx_66xxx|ht32f66xxx|ht32f65xxx|ht32)_(?P<module>.+)$"
|
|
17
|
+
)
|
|
18
|
+
# 例程 readme 中的芯片型号 token(HT32F52352 / HT50F32002 …)
|
|
19
|
+
_CHIP_RE = re.compile(r"\bHT(?:32|50)\w{3,}\b")
|
|
20
|
+
# FWLib 包名 → 系列标签(5xxxx → HT32F5 / M0+)
|
|
21
|
+
_SERIES_MAP = {
|
|
22
|
+
"5xxxx": ("HT32F5", "M0+"),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
BRIEF_MAX = 200
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _now() -> str:
|
|
29
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _rel(posix_path: str) -> str:
|
|
33
|
+
return posix_path.replace("\\", "/")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _parse_readme(readme: Path) -> tuple[str, list[str]]:
|
|
37
|
+
"""从官方例程 readme.txt 提取简介与标注芯片。"""
|
|
38
|
+
if not readme.is_file():
|
|
39
|
+
return "", []
|
|
40
|
+
try:
|
|
41
|
+
text = readme.read_text(encoding="utf-8", errors="replace")
|
|
42
|
+
except OSError:
|
|
43
|
+
return "", []
|
|
44
|
+
brief = ""
|
|
45
|
+
marker = "@par Example Description:"
|
|
46
|
+
i = text.find(marker)
|
|
47
|
+
if i >= 0:
|
|
48
|
+
seg = text[i + len(marker):]
|
|
49
|
+
j = seg.find("@par")
|
|
50
|
+
if j >= 0:
|
|
51
|
+
seg = seg[:j]
|
|
52
|
+
brief = " ".join(seg.split())[:BRIEF_MAX]
|
|
53
|
+
chips = sorted(set(_CHIP_RE.findall(text)))[:16]
|
|
54
|
+
return brief, chips
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CodeIndex:
|
|
58
|
+
"""索引构建与加载(构建慢扫描一次,之后秒级加载 JSON)。"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, code_root: Path, index_file: Path | None = None) -> None:
|
|
61
|
+
self.root = Path(code_root)
|
|
62
|
+
self.index_file = Path(index_file) if index_file else (
|
|
63
|
+
self.root / ".work" / "mcp-server" / "index" / "code_index.json"
|
|
64
|
+
)
|
|
65
|
+
self.data: dict[str, Any] = {}
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------ 加载
|
|
68
|
+
def load(self) -> "CodeIndex":
|
|
69
|
+
"""启动即加载:索引存在则读 JSON,否则现场构建并落盘。"""
|
|
70
|
+
if self.index_file.is_file():
|
|
71
|
+
with open(self.index_file, encoding="utf-8") as f:
|
|
72
|
+
self.data = json.load(f)
|
|
73
|
+
else:
|
|
74
|
+
self.build(force=True)
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------ 构建
|
|
78
|
+
def build(self, force: bool = False) -> dict[str, Any]:
|
|
79
|
+
if not force and self.index_file.is_file():
|
|
80
|
+
return self.load().data
|
|
81
|
+
data: dict[str, Any] = {
|
|
82
|
+
"built_at": _now(),
|
|
83
|
+
"code_version": self._read_version(),
|
|
84
|
+
"fwlibs": [],
|
|
85
|
+
"templates": [],
|
|
86
|
+
"knowledge_pages": [],
|
|
87
|
+
"files": [],
|
|
88
|
+
"stats": {},
|
|
89
|
+
}
|
|
90
|
+
raw_dir = self.root / "raw"
|
|
91
|
+
if raw_dir.is_dir():
|
|
92
|
+
data["fwlibs"] = [self._scan_fwlib(p) for p in sorted(raw_dir.iterdir())
|
|
93
|
+
if p.is_dir() and p.name != "templates"]
|
|
94
|
+
data["templates"] = [self._scan_template(p) for p in sorted((raw_dir / "templates").iterdir())
|
|
95
|
+
if p.is_dir()] if (raw_dir / "templates").is_dir() else []
|
|
96
|
+
data["files"] = sorted(
|
|
97
|
+
_rel(f.relative_to(self.root).as_posix())
|
|
98
|
+
for f in raw_dir.rglob("*") if f.is_file()
|
|
99
|
+
)
|
|
100
|
+
data["knowledge_pages"] = self._scan_knowledge()
|
|
101
|
+
fw = data["fwlibs"]
|
|
102
|
+
data["stats"] = {
|
|
103
|
+
"fwlib_packages": len(fw),
|
|
104
|
+
"library_modules": len({m["module"] for p in fw for m in p["modules"]}),
|
|
105
|
+
"examples": sum(len(p["examples"]) for p in fw),
|
|
106
|
+
"example_peripherals": len({e["peripheral"] for p in fw for e in p["examples"]}),
|
|
107
|
+
"templates": len(data["templates"]),
|
|
108
|
+
"raw_files": len(data["files"]),
|
|
109
|
+
"knowledge_pages": len(data["knowledge_pages"]),
|
|
110
|
+
}
|
|
111
|
+
self.data = data
|
|
112
|
+
self.index_file.parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
with open(self.index_file, "w", encoding="utf-8") as f:
|
|
114
|
+
json.dump(data, f, ensure_ascii=False, indent=1)
|
|
115
|
+
return data
|
|
116
|
+
|
|
117
|
+
# ------------------------------------------------------------ 子扫描
|
|
118
|
+
def _read_version(self) -> str:
|
|
119
|
+
v = self.root / "VERSION"
|
|
120
|
+
if v.is_file():
|
|
121
|
+
for line in v.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
122
|
+
line = line.strip()
|
|
123
|
+
if line:
|
|
124
|
+
return line
|
|
125
|
+
return "unknown"
|
|
126
|
+
|
|
127
|
+
def _scan_fwlib(self, pkg: Path) -> dict[str, Any]:
|
|
128
|
+
rel_pkg = _rel(pkg.relative_to(self.root).as_posix())
|
|
129
|
+
m = re.match(r"HT32_STD_(?P<key>\w+?)_FWLib", pkg.name)
|
|
130
|
+
series = []
|
|
131
|
+
family = ""
|
|
132
|
+
if m and m.group(1) in _SERIES_MAP:
|
|
133
|
+
series = [_SERIES_MAP[m.group(1)][0]]
|
|
134
|
+
family = _SERIES_MAP[m.group(1)][1]
|
|
135
|
+
info: dict[str, Any] = {
|
|
136
|
+
"name": pkg.name,
|
|
137
|
+
"path": rel_pkg,
|
|
138
|
+
"series": series,
|
|
139
|
+
"family": family,
|
|
140
|
+
"modules": [],
|
|
141
|
+
"examples": [],
|
|
142
|
+
}
|
|
143
|
+
# 驱动模块:src/*.c(一个 .c 一个驱动变体,同模块名合并展示)
|
|
144
|
+
for src in sorted(pkg.rglob("HT32F5xxxx_Driver/src/*.c")):
|
|
145
|
+
stem = src.stem
|
|
146
|
+
mm = _MODULE_PREFIX_RE.match(stem)
|
|
147
|
+
module = mm.group("module") if mm else stem
|
|
148
|
+
inc = src.with_name(stem + ".h")
|
|
149
|
+
entry = {
|
|
150
|
+
"module": module,
|
|
151
|
+
"src": _rel(src.relative_to(self.root).as_posix()),
|
|
152
|
+
"inc": _rel(inc.relative_to(self.root).as_posix()) if inc.is_file() else "",
|
|
153
|
+
}
|
|
154
|
+
info["modules"].append(entry)
|
|
155
|
+
# 例程:example/ 下任何含 main.c 的目录(外设=第一层目录名)
|
|
156
|
+
example_dir = pkg / "example"
|
|
157
|
+
if example_dir.is_dir():
|
|
158
|
+
for main_c in sorted(example_dir.rglob("main.c")):
|
|
159
|
+
ex_dir = main_c.parent
|
|
160
|
+
rel = ex_dir.relative_to(example_dir)
|
|
161
|
+
parts = rel.parts
|
|
162
|
+
brief, chips = _parse_readme(ex_dir / "readme.txt")
|
|
163
|
+
info["examples"].append({
|
|
164
|
+
"peripheral": parts[0],
|
|
165
|
+
"name": "/".join(parts[1:]) if len(parts) > 1 else parts[0],
|
|
166
|
+
"path": _rel(ex_dir.relative_to(self.root).as_posix()),
|
|
167
|
+
"brief": brief,
|
|
168
|
+
"chips": chips,
|
|
169
|
+
"files": [f.name for f in sorted(ex_dir.iterdir()) if f.is_file()],
|
|
170
|
+
})
|
|
171
|
+
return info
|
|
172
|
+
|
|
173
|
+
def _scan_template(self, tpl: Path) -> dict[str, Any]:
|
|
174
|
+
rel_tpl = _rel(tpl.relative_to(self.root).as_posix())
|
|
175
|
+
entry: dict[str, Any] = {
|
|
176
|
+
"name": tpl.name,
|
|
177
|
+
"path": rel_tpl,
|
|
178
|
+
"default_device": "",
|
|
179
|
+
"device_count": 0,
|
|
180
|
+
"devices": [],
|
|
181
|
+
}
|
|
182
|
+
dj = tpl / "devices.json"
|
|
183
|
+
if dj.is_file():
|
|
184
|
+
try:
|
|
185
|
+
meta = json.loads(dj.read_text(encoding="utf-8"))
|
|
186
|
+
devices = meta.get("devices", [])
|
|
187
|
+
entry["default_device"] = meta.get("default_device", "")
|
|
188
|
+
entry["device_count"] = len(devices)
|
|
189
|
+
entry["devices"] = [d.get("device", "") for d in devices]
|
|
190
|
+
except (OSError, json.JSONDecodeError):
|
|
191
|
+
pass
|
|
192
|
+
return entry
|
|
193
|
+
|
|
194
|
+
def _scan_knowledge(self) -> list[dict[str, Any]]:
|
|
195
|
+
pages: list[dict[str, Any]] = []
|
|
196
|
+
for sub, ptype in (("entities/libraries", "entity"),
|
|
197
|
+
("entities", "entity"),
|
|
198
|
+
("comparisons", "comparison")):
|
|
199
|
+
d = self.root / sub
|
|
200
|
+
if not d.is_dir():
|
|
201
|
+
continue
|
|
202
|
+
for md in sorted(d.rglob("*.md")):
|
|
203
|
+
title, tags = _parse_frontmatter(md)
|
|
204
|
+
pages.append({
|
|
205
|
+
"title": title or md.stem,
|
|
206
|
+
"type": ptype,
|
|
207
|
+
"tags": tags,
|
|
208
|
+
"path": _rel(md.relative_to(self.root).as_posix()),
|
|
209
|
+
})
|
|
210
|
+
# 去重(entities/libraries 会命中 entities 递归两次)
|
|
211
|
+
seen: set[str] = set()
|
|
212
|
+
uniq = []
|
|
213
|
+
for p in pages:
|
|
214
|
+
if p["path"] not in seen:
|
|
215
|
+
seen.add(p["path"])
|
|
216
|
+
uniq.append(p)
|
|
217
|
+
return uniq
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _parse_frontmatter(md: Path) -> tuple[str, list[str]]:
|
|
221
|
+
title, tags = "", []
|
|
222
|
+
try:
|
|
223
|
+
text = md.read_text(encoding="utf-8", errors="replace")
|
|
224
|
+
except OSError:
|
|
225
|
+
return "", []
|
|
226
|
+
if text.startswith("---"):
|
|
227
|
+
end = text.find("\n---", 3)
|
|
228
|
+
fm = text[3:end] if end > 0 else ""
|
|
229
|
+
for line in fm.splitlines():
|
|
230
|
+
if line.startswith("title:"):
|
|
231
|
+
title = line.split(":", 1)[1].strip()
|
|
232
|
+
elif line.startswith("tags:"):
|
|
233
|
+
tags = [t.strip() for t in line.split(":", 1)[1].strip("[]").split(",") if t.strip()]
|
|
234
|
+
return title, tags
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ---------------------------------------------------------------- 查询辅助
|
|
238
|
+
def normalize_series(series: str) -> str:
|
|
239
|
+
"""f5 / F5 / HT32F5 / 5xxxx → f5(用于系列过滤匹配)。"""
|
|
240
|
+
s = series.strip().lower()
|
|
241
|
+
for prefix in ("ht32f", "ht32", "ht"):
|
|
242
|
+
if s.startswith(prefix):
|
|
243
|
+
s = s[len(prefix):]
|
|
244
|
+
break
|
|
245
|
+
s = s.replace("xxxx", "").replace("_", "")
|
|
246
|
+
return s
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""知识页检索:entities/ 与 comparisons/ 的 .md 页面(词法打分,无向量层)。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_TOKEN_RE = re.compile(r"[a-z0-9_]+|[\u4e00-\u9fff]+")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class KnowledgePage:
|
|
13
|
+
title: str
|
|
14
|
+
path: str
|
|
15
|
+
tags: list[str] = field(default_factory=list)
|
|
16
|
+
content: str = ""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class KnowledgeSearch:
|
|
20
|
+
"""加载并按相关性打分检索知识页(页面少,启动后首次查询时加载全文)。"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, code_root: Path) -> None:
|
|
23
|
+
self.root = Path(code_root)
|
|
24
|
+
self._pages: list[KnowledgePage] | None = None
|
|
25
|
+
|
|
26
|
+
def _ensure(self) -> list[KnowledgePage]:
|
|
27
|
+
if self._pages is None:
|
|
28
|
+
pages: list[KnowledgePage] = []
|
|
29
|
+
for sub in ("entities/libraries", "comparisons"):
|
|
30
|
+
d = self.root / sub
|
|
31
|
+
if not d.is_dir():
|
|
32
|
+
continue
|
|
33
|
+
for md in sorted(d.rglob("*.md")):
|
|
34
|
+
try:
|
|
35
|
+
text = md.read_text(encoding="utf-8", errors="replace")
|
|
36
|
+
except OSError:
|
|
37
|
+
continue
|
|
38
|
+
pages.append(KnowledgePage(
|
|
39
|
+
title=md.stem,
|
|
40
|
+
path=md.relative_to(self.root).as_posix(),
|
|
41
|
+
tags=_extract_tags(text),
|
|
42
|
+
content=text,
|
|
43
|
+
))
|
|
44
|
+
self._pages = pages
|
|
45
|
+
return self._pages
|
|
46
|
+
|
|
47
|
+
def search(self, query: str, limit: int = 5) -> list[tuple[KnowledgePage, float, str]]:
|
|
48
|
+
"""返回 (页面, 得分, 片段) 列表,按得分降序。"""
|
|
49
|
+
tokens = _TOKEN_RE.findall(query.lower())
|
|
50
|
+
if not tokens:
|
|
51
|
+
return []
|
|
52
|
+
scored: list[tuple[KnowledgePage, float, str]] = []
|
|
53
|
+
for page in self._ensure():
|
|
54
|
+
low = page.content.lower()
|
|
55
|
+
title = page.title.lower()
|
|
56
|
+
score = 0.0
|
|
57
|
+
for t in tokens:
|
|
58
|
+
if t in title:
|
|
59
|
+
score += 6
|
|
60
|
+
score += sum(4 if t == tag else (2 if t in tag else 0)
|
|
61
|
+
for tag in (p.lower() for p in page.tags))
|
|
62
|
+
score += min(low.count(t), 8)
|
|
63
|
+
if score <= 0:
|
|
64
|
+
continue
|
|
65
|
+
scored.append((page, score, _snippet(page.content, tokens)))
|
|
66
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
67
|
+
return scored[:limit]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _extract_tags(text: str) -> list[str]:
|
|
71
|
+
if not text.startswith("---"):
|
|
72
|
+
return []
|
|
73
|
+
end = text.find("\n---", 3)
|
|
74
|
+
fm = text[3:end] if end > 0 else ""
|
|
75
|
+
for line in fm.splitlines():
|
|
76
|
+
if line.startswith("tags:"):
|
|
77
|
+
return [t.strip() for t in line.split(":", 1)[1].strip("[]").split(",") if t.strip()]
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _snippet(content: str, tokens: list[str], width: int = 600) -> str:
|
|
82
|
+
"""取命中 token 最多的行为锚点,截取附近片段。"""
|
|
83
|
+
lines = content.splitlines()
|
|
84
|
+
best_i, best_hits = 0, -1
|
|
85
|
+
for i, line in enumerate(lines):
|
|
86
|
+
if not line.strip() or line.startswith("---"):
|
|
87
|
+
continue
|
|
88
|
+
low = line.lower()
|
|
89
|
+
hits = sum(low.count(t) for t in tokens)
|
|
90
|
+
if hits > best_hits:
|
|
91
|
+
best_i, best_hits = i, hits
|
|
92
|
+
chunk: list[str] = []
|
|
93
|
+
total = 0
|
|
94
|
+
for line in lines[best_i:]:
|
|
95
|
+
chunk.append(line)
|
|
96
|
+
total += len(line) + 1
|
|
97
|
+
if total >= width:
|
|
98
|
+
break
|
|
99
|
+
snippet = "\n".join(chunk).strip()
|
|
100
|
+
return snippet[:width] + ("…" if len(snippet) > width else "")
|
ht32_code_mcp/server.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""HT32 Code MCP Server。
|
|
2
|
+
|
|
3
|
+
提供函数库/例程/模板检索与工程导出,供 AI Agent 调用。
|
|
4
|
+
传输协议:stdio(P0 本地阶段;云端阶段见总纲 5.3)。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from importlib.metadata import PackageNotFoundError, version as pkg_version
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from fastmcp import FastMCP
|
|
14
|
+
|
|
15
|
+
from .exporter import ExportError, ProjectExporter
|
|
16
|
+
from .index import CodeIndex, normalize_series
|
|
17
|
+
from .knowledge import KnowledgeSearch
|
|
18
|
+
|
|
19
|
+
# 知识库根目录(ht32-code 仓库根;src/ht32_code_mcp → 上溯 4 级)
|
|
20
|
+
DEFAULT_CODE_ROOT = Path(__file__).resolve().parents[4]
|
|
21
|
+
CODE_ROOT = Path(os.environ.get("CODE_ROOT", DEFAULT_CODE_ROOT))
|
|
22
|
+
|
|
23
|
+
index = CodeIndex(CODE_ROOT).load()
|
|
24
|
+
knowledge = KnowledgeSearch(CODE_ROOT)
|
|
25
|
+
exporter = ProjectExporter(CODE_ROOT)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
_MCP_VERSION = pkg_version("ht32-code-mcp")
|
|
29
|
+
except PackageNotFoundError:
|
|
30
|
+
_MCP_VERSION = "unknown (开发模式)"
|
|
31
|
+
|
|
32
|
+
_INSTRUCTIONS = """HT32 代码知识库(函数库/例程/工程模板),当前覆盖 HT32F5(M0+,官方固件库 V1.21.1 原档)。
|
|
33
|
+
写 HT32 代码必须基于本库真实函数库知识,不要编造 API/宏/路径。
|
|
34
|
+
|
|
35
|
+
## 按需求选工具
|
|
36
|
+
- 确认版本/覆盖范围 → get_code_version
|
|
37
|
+
- 问"某外设库怎么用/有哪些模块" → list_libraries → search_code_knowledge(模块名)
|
|
38
|
+
- 找官方例程(按外设/系列/芯片) → find_example → get_file(例程路径) 看原码
|
|
39
|
+
- 取 raw/ 真实源文件(驱动 .c/.h、例程、devices.json 等) → get_file
|
|
40
|
+
- 检索知识页(初始化套路/易错点/子系列差异) → search_code_knowledge
|
|
41
|
+
- 新建工程 → get_project_manifest 看模板 → export_project_bundle(chip, out_dir) 导出自包含工程
|
|
42
|
+
|
|
43
|
+
## 通用约定
|
|
44
|
+
- 路径一律为仓库相对 posix 路径(raw/... 开头),get_file 直接可用
|
|
45
|
+
- 型号大小写自动归一(ht32f52352 = HT32F52352)
|
|
46
|
+
- 知识页查不到时不要编造,改查 list_libraries + get_file 原码
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
mcp = FastMCP("ht32-code", instructions=_INSTRUCTIONS)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ================================================================ MCP Tools
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _series_of_chip(chip: str) -> tuple[str | None, str | None]:
|
|
56
|
+
"""芯片型号 → (系列标签, 模板名)。对照模板 devices.json(如 HT32F52352 → f5, ht32f5-keil)。"""
|
|
57
|
+
want = chip.strip().upper()
|
|
58
|
+
for t in index.data.get("templates", []):
|
|
59
|
+
if any(d.upper() == want for d in t.get("devices", []) if d):
|
|
60
|
+
return normalize_series(t["name"].split("-")[0]), t["name"]
|
|
61
|
+
return None, None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@mcp.tool()
|
|
65
|
+
def get_code_version() -> str:
|
|
66
|
+
"""获取代码知识库版本与 manifest 统计信息。
|
|
67
|
+
|
|
68
|
+
返回:知识库版本(VERSION 文件)、固件库覆盖、模板状态、
|
|
69
|
+
库模块/例程/知识页/raw 文件统计与索引构建时间。
|
|
70
|
+
"""
|
|
71
|
+
version_file = CODE_ROOT / "VERSION"
|
|
72
|
+
version_text = version_file.read_text(encoding="utf-8", errors="replace").strip() \
|
|
73
|
+
if version_file.is_file() else "(VERSION 文件缺失)"
|
|
74
|
+
stats = index.data.get("stats", {})
|
|
75
|
+
lines = [
|
|
76
|
+
f"ht32-code 知识库版本:{index.data.get('code_version', 'unknown')}",
|
|
77
|
+
f"MCP server:ht32-code-mcp {_MCP_VERSION}",
|
|
78
|
+
"",
|
|
79
|
+
"VERSION 文件内容:",
|
|
80
|
+
version_text,
|
|
81
|
+
"",
|
|
82
|
+
"索引统计(预构建 JSON 索引,built_at "
|
|
83
|
+
f"{index.data.get('built_at', '?')}):",
|
|
84
|
+
f"- 固件库包:{stats.get('fwlib_packages', 0)}",
|
|
85
|
+
f"- 库模块:{stats.get('library_modules', 0)}",
|
|
86
|
+
f"- 例程:{stats.get('examples', 0)}({stats.get('example_peripherals', 0)} 个外设目录)",
|
|
87
|
+
f"- 工程模板:{stats.get('templates', 0)}",
|
|
88
|
+
f"- 知识页:{stats.get('knowledge_pages', 0)}",
|
|
89
|
+
f"- raw/ 文件总数:{stats.get('raw_files', 0)}",
|
|
90
|
+
]
|
|
91
|
+
return "\n".join(lines)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@mcp.tool()
|
|
95
|
+
def list_libraries() -> str:
|
|
96
|
+
"""枚举函数库模块与覆盖系列。
|
|
97
|
+
|
|
98
|
+
返回每个库模块的驱动源码/头文件路径(raw/)、变体数、
|
|
99
|
+
对应官方例程数与知识页路径(entities/libraries/)。
|
|
100
|
+
"""
|
|
101
|
+
lines = ["库模块清单(模块名按官方驱动 .c 文件名归一):"]
|
|
102
|
+
for pkg in index.data.get("fwlibs", []):
|
|
103
|
+
series = "/".join(pkg.get("series") or ["?"]) + (f"({pkg['family']})" if pkg.get("family") else "")
|
|
104
|
+
ex_counts: dict[str, int] = {}
|
|
105
|
+
for ex in pkg.get("examples", []):
|
|
106
|
+
ex_counts[ex["peripheral"].lower()] = ex_counts.get(ex["peripheral"].lower(), 0) + 1
|
|
107
|
+
page_paths = {p["path"] for p in index.data.get("knowledge_pages", [])}
|
|
108
|
+
modules: dict[str, list[dict]] = {}
|
|
109
|
+
for m in pkg.get("modules", []):
|
|
110
|
+
modules.setdefault(m["module"], []).append(m)
|
|
111
|
+
lines.append("")
|
|
112
|
+
lines.append(f"== 固件库 {pkg['name']}(系列 {series})== "
|
|
113
|
+
f"共 {len(modules)} 个模块")
|
|
114
|
+
for mod in sorted(modules):
|
|
115
|
+
variants = modules[mod]
|
|
116
|
+
srcs = "、".join(v["src"] for v in variants)
|
|
117
|
+
page = f"entities/libraries/{mod}.md"
|
|
118
|
+
page_note = page if page in page_paths else "—"
|
|
119
|
+
lines.append(
|
|
120
|
+
f"- {mod}:{len(variants)} 个驱动变体 | {srcs}"
|
|
121
|
+
f" | 例程 {ex_counts.get(mod, 0)} 个 | 知识页 {page_note}"
|
|
122
|
+
)
|
|
123
|
+
lines.append("")
|
|
124
|
+
lines.append("用法:模块详情查 search_code_knowledge(模块名);原码用 get_file(路径)。")
|
|
125
|
+
return "\n".join(lines)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@mcp.tool()
|
|
129
|
+
def find_example(peripheral: str, series: str = "", chip: str = "", limit: int = 10) -> str:
|
|
130
|
+
"""按外设/系列/芯片查找官方例程,返回路径与简介。
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
peripheral: 外设名(如 gpio / usart / adc / gptm),大小写不敏感,
|
|
134
|
+
支持精确或包含匹配(usart 亦命中 UART 目录的别名说明)
|
|
135
|
+
series: 系列过滤(f5 / HT32F5 / M0+),默认不过滤
|
|
136
|
+
chip: 芯片型号(如 HT32F52352)。官方例程为家族通用(readme 不标注
|
|
137
|
+
具体型号),本参数先对照模板 devices.json 确认型号受支持,
|
|
138
|
+
再按其所属系列过滤例程
|
|
139
|
+
limit: 返回上限,默认 10
|
|
140
|
+
"""
|
|
141
|
+
pool = list(index.data.get("fwlibs", []))
|
|
142
|
+
chip_note = ""
|
|
143
|
+
if chip:
|
|
144
|
+
chip_series, tpl = _series_of_chip(chip.strip())
|
|
145
|
+
if chip_series is None:
|
|
146
|
+
devices: list[str] = []
|
|
147
|
+
for t in index.data.get("templates", []):
|
|
148
|
+
devices += [d for d in t.get("devices", []) if d]
|
|
149
|
+
return (f"芯片 '{chip}' 不在支持列表(当前 {len(devices)} 个型号,"
|
|
150
|
+
"见 get_project_manifest / devices.json)。\n"
|
|
151
|
+
f"可用示例(前 20):{', '.join(devices[:20])}…")
|
|
152
|
+
pool = [p for p in pool
|
|
153
|
+
if chip_series in [normalize_series(s) for s in p.get("series", [])]]
|
|
154
|
+
chip_note = f"\n(例程为家族通用,已按 {chip.strip().upper()} 所属系列过滤;" \
|
|
155
|
+
f"型号已在模板 {tpl} 支持列表确认)"
|
|
156
|
+
if series:
|
|
157
|
+
want = normalize_series(series)
|
|
158
|
+
want_family = series.strip().lower().rstrip("+")
|
|
159
|
+
pool = [p for p in pool
|
|
160
|
+
if any(normalize_series(s) == want for s in p.get("series", []))
|
|
161
|
+
or p.get("family", "").lower() == want_family]
|
|
162
|
+
if not pool:
|
|
163
|
+
return f"系列 '{series}' 不在覆盖范围。当前覆盖:" + "、".join(
|
|
164
|
+
f"{p['name']}" for p in index.data.get("fwlibs", []))
|
|
165
|
+
query = peripheral.strip().lower()
|
|
166
|
+
matches: list[dict] = []
|
|
167
|
+
for p in pool:
|
|
168
|
+
for ex in p.get("examples", []):
|
|
169
|
+
hay = f"{ex['peripheral']}/{ex['name']}".lower()
|
|
170
|
+
if ex["peripheral"].lower() == query or query in hay:
|
|
171
|
+
matches.append(ex)
|
|
172
|
+
if not matches:
|
|
173
|
+
known = sorted({ex["peripheral"] for p in pool for ex in p.get("examples", [])})
|
|
174
|
+
return (f"未找到外设 '{peripheral}' 的例程。可用外设目录({len(known)} 个):"
|
|
175
|
+
+ "、".join(known))
|
|
176
|
+
chip_filter_note = chip_note
|
|
177
|
+
matches = matches[:limit]
|
|
178
|
+
lines = [f"找到 {len(matches)} 个 '{peripheral}' 例程:{chip_filter_note}"]
|
|
179
|
+
for ex in matches:
|
|
180
|
+
brief = f" — {ex['brief']}" if ex.get("brief") else ""
|
|
181
|
+
files = ", ".join(ex.get("files", []))
|
|
182
|
+
lines.append(f"- {ex['peripheral']}/{ex['name']}{brief}\n"
|
|
183
|
+
f" 路径: {ex['path']}\n 文件: {files}")
|
|
184
|
+
lines.append("原码获取:get_file(\"<路径>/main.c\");知识页:search_code_knowledge(\""
|
|
185
|
+
+ peripheral.lower() + "\")。")
|
|
186
|
+
return "\n".join(lines)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@mcp.tool()
|
|
190
|
+
def get_file(path: str, max_length: int = 50000) -> str:
|
|
191
|
+
"""获取 raw/ 下真实文件内容(只读原档,字节冻结)。
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
path: 仓库相对路径(raw/... 开头;可省略 raw/ 前缀自动补全),
|
|
195
|
+
如 "raw/HT32_STD_5xxxx_FWLib_V1.21.1/library/HT32F5xxxx_Driver/src/ht32f5xxxx_gpio.c"
|
|
196
|
+
max_length: 返回最大字符数,默认 50000;-1 取全文
|
|
197
|
+
"""
|
|
198
|
+
norm = path.strip().replace("\\", "/")
|
|
199
|
+
while norm.startswith("./"):
|
|
200
|
+
norm = norm[2:]
|
|
201
|
+
if not norm:
|
|
202
|
+
return "错误:path 不能为空。"
|
|
203
|
+
if len(norm) > 2 and norm[1] == ":" or norm.startswith("/"):
|
|
204
|
+
return f"错误:只接受仓库相对路径(raw/...),拒绝绝对路径:{path}"
|
|
205
|
+
parts = norm.split("/")
|
|
206
|
+
if ".." in parts:
|
|
207
|
+
return f"错误:路径不允许包含 '..':{path}"
|
|
208
|
+
if not norm.startswith("raw/"):
|
|
209
|
+
norm = "raw/" + norm
|
|
210
|
+
known = set(index.data.get("files", []))
|
|
211
|
+
target = CODE_ROOT / Path(*norm.split("/"))
|
|
212
|
+
if norm not in known and not target.is_file():
|
|
213
|
+
return (f"错误:文件不存在:{norm}\n"
|
|
214
|
+
f"提示:路径须为 raw/ 下真实文件(可用 find_example / list_libraries 取路径)。")
|
|
215
|
+
raw_root = (CODE_ROOT / "raw").resolve()
|
|
216
|
+
if raw_root not in target.resolve().parents:
|
|
217
|
+
return f"错误:路径越界(只允许 raw/ 下文件):{path}"
|
|
218
|
+
try:
|
|
219
|
+
blob = target.read_bytes()
|
|
220
|
+
except OSError as e:
|
|
221
|
+
return f"错误:读取失败:{e}"
|
|
222
|
+
if b"\x00" in blob[:8192]:
|
|
223
|
+
return (f"错误:'{norm}' 是二进制文件({len(blob)} 字节),不提供内容读取;"
|
|
224
|
+
"文本源码/头文件/例程均可读取。")
|
|
225
|
+
text = blob.decode("utf-8", errors="replace")
|
|
226
|
+
if max_length >= 0 and len(text) > max_length:
|
|
227
|
+
return (f"[{norm}](截取前 {max_length}/{len(text)} 字符,"
|
|
228
|
+
f"如需更多请重传 max_length={min(max_length * 2, len(text))} 或 -1 取全文)\n\n"
|
|
229
|
+
+ text[:max_length])
|
|
230
|
+
return text
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@mcp.tool()
|
|
234
|
+
def search_code_knowledge(query: str, limit: int = 5) -> str:
|
|
235
|
+
"""检索知识页(entities/libraries/ 函数库用法 + comparisons/ 系列差异)。
|
|
236
|
+
|
|
237
|
+
适合:库模块初始化套路、API 易错点、子系列差异、换型号 checklist。
|
|
238
|
+
知识页只写使用知识;原码用 find_example + get_file 获取。
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
query: 关键词,如 "GPIO 输出初始化"、"USART 中断"、"子系列差异"、"libcfg"
|
|
242
|
+
limit: 返回上限,默认 5
|
|
243
|
+
"""
|
|
244
|
+
results = knowledge.search(query, limit=limit)
|
|
245
|
+
if not results:
|
|
246
|
+
return ("未找到匹配知识页。建议:换模块官方名小写重试(gpio/usart/adc…),"
|
|
247
|
+
"或用 list_libraries 看模块清单后 get_file 直取原码。")
|
|
248
|
+
lines = [f"找到 {len(results)} 条结果(按相关性排序):"]
|
|
249
|
+
for i, (page, score, snippet) in enumerate(results, 1):
|
|
250
|
+
lines.append(f"--- 结果 {i} [{page.title}](相关度 {score:.0f})---")
|
|
251
|
+
lines.append(f"路径: {page.path}")
|
|
252
|
+
lines.append(snippet)
|
|
253
|
+
lines.append("")
|
|
254
|
+
return "\n".join(lines)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@mcp.tool()
|
|
258
|
+
def get_project_manifest() -> str:
|
|
259
|
+
"""获取工程模板清单(raw/templates/)。
|
|
260
|
+
|
|
261
|
+
返回模板结构、默认芯片、支持的型号数与用法;配合 export_project_bundle
|
|
262
|
+
导出自包含可编译工程。
|
|
263
|
+
"""
|
|
264
|
+
templates = index.data.get("templates", [])
|
|
265
|
+
if not templates:
|
|
266
|
+
return "raw/templates/ 下暂无模板。"
|
|
267
|
+
lines = [f"工程模板清单({len(templates)} 个):"]
|
|
268
|
+
for t in templates:
|
|
269
|
+
lines.append("")
|
|
270
|
+
lines.append(f"== {t['name']} ==")
|
|
271
|
+
lines.append(f"路径: {t['path']}")
|
|
272
|
+
lines.append(f"默认芯片: {t.get('default_device') or '—'}")
|
|
273
|
+
lines.append(f"支持型号: {t.get('device_count', 0)} 个(devices.json,"
|
|
274
|
+
"完整参数表可 get_file(\"" + t["path"] + "/devices.json\"))")
|
|
275
|
+
lines.append(
|
|
276
|
+
"结构: Application/(main、it、conf)+ CMSIS/(设备头+system)+ "
|
|
277
|
+
"Drivers/(最小驱动集+libcfg)+ Startup/ + MDK-ARM/(Keil 工程文件)"
|
|
278
|
+
)
|
|
279
|
+
lines.append("")
|
|
280
|
+
lines.append("导出工程:export_project_bundle(chip, out_dir) —— 按型号参数化"
|
|
281
|
+
"(Device/C 宏/汇编宏/system/startup/libcfg)并注入官方库源码,"
|
|
282
|
+
"产出自包含可编译工程。")
|
|
283
|
+
return "\n".join(lines)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@mcp.tool()
|
|
287
|
+
def export_project_bundle(chip: str, out_dir: str) -> str:
|
|
288
|
+
"""按芯片型号从模板组装自包含可编译 Keil 工程到本地目录。
|
|
289
|
+
|
|
290
|
+
动作:复制 raw/templates/ht32f5-keil 模板 → 按 devices.json 参数化
|
|
291
|
+
uvprojx(Device、C 宏、汇编宏)→ 从官方库注入对应 system/startup/libcfg
|
|
292
|
+
文件。产物可直接用 Keil MDK 打开编译(需 HT32_DFP pack)。
|
|
293
|
+
本阶段为本地目录导出;云端 token 下载 URL 后续阶段扩展。
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
chip: 芯片型号,如 "HT32F52352"(大小写不敏感)
|
|
297
|
+
out_dir: 输出目录(须为空目录或不存在;不会覆盖已有内容)
|
|
298
|
+
"""
|
|
299
|
+
try:
|
|
300
|
+
return exporter.export(chip, out_dir)
|
|
301
|
+
except ExportError as e:
|
|
302
|
+
return f"导出失败:{e}"
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ================================================================ 入口
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def main() -> None:
|
|
309
|
+
"""入口:按 MCP_TRANSPORT 环境变量启动(默认 stdio,P0 本地阶段)。"""
|
|
310
|
+
transport = os.environ.get("MCP_TRANSPORT", "stdio").lower()
|
|
311
|
+
stats = index.data.get("stats", {})
|
|
312
|
+
print(f"[ht32-code-mcp] 程序版本: ht32-code-mcp {_MCP_VERSION}", file=sys.stderr)
|
|
313
|
+
print(f"[ht32-code-mcp] 知识库版本: {index.data.get('code_version', '?')}"
|
|
314
|
+
f"(索引 built_at {index.data.get('built_at', '?')})", file=sys.stderr)
|
|
315
|
+
print(f"[ht32-code-mcp] 知识库根目录: {CODE_ROOT}", file=sys.stderr)
|
|
316
|
+
print(f"[ht32-code-mcp] 索引就绪: {stats.get('library_modules', 0)} 模块 / "
|
|
317
|
+
f"{stats.get('examples', 0)} 例程 / {stats.get('raw_files', 0)} raw 文件", file=sys.stderr)
|
|
318
|
+
if transport == "stdio":
|
|
319
|
+
print("[ht32-code-mcp] 启动服务: stdio", file=sys.stderr)
|
|
320
|
+
mcp.run(transport="stdio")
|
|
321
|
+
else:
|
|
322
|
+
print(f"[ht32-code-mcp] 传输模式 '{transport}' 暂未启用(P0 仅 stdio)", file=sys.stderr)
|
|
323
|
+
sys.exit(2)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ht32-code-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: firmware,holtek,ht32,mcp,mcu
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Requires-Dist: fastmcp>=0.5.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
ht32_code_mcp/__init__.py,sha256=SBNKjUnBKjYQtw0TidyBOx2NFTcSpCJ-Gh9x7oujINs,109
|
|
2
|
+
ht32_code_mcp/__main__.py,sha256=P3nY1dMtxQ9y8V1VICOufkO950OKDMRHIE-vXy8w3Aw,88
|
|
3
|
+
ht32_code_mcp/exporter.py,sha256=vTCvVAZoydf-b7h3XpEo1ubIEsgz2CazMS8ECYHJQ8M,8177
|
|
4
|
+
ht32_code_mcp/index.py,sha256=S0ghDMeyCSfI4NrTI1eOXI4FJQm_zHLtQHMOuqQR8pk,9622
|
|
5
|
+
ht32_code_mcp/knowledge.py,sha256=P8XlNlCdDRzksQKqgkCzjFZkIfprIBoePPtRCEcM1qE,3566
|
|
6
|
+
ht32_code_mcp/server.py,sha256=mUjfGo_2EI8YksIrtBTEQ8wCszQXKX6l1kzVe2DhcG0,15180
|
|
7
|
+
ht32_code_mcp-0.1.0.dist-info/METADATA,sha256=Gz4vS8AoOoCN-q5_vMkUucSRY2EWQM_m-48Sizs_WfE,595
|
|
8
|
+
ht32_code_mcp-0.1.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
|
|
9
|
+
ht32_code_mcp-0.1.0.dist-info/entry_points.txt,sha256=W0VlSn_R5exzRG2xKs_1kjayo0KEmeh8DphQKbUZPMo,60
|
|
10
|
+
ht32_code_mcp-0.1.0.dist-info/RECORD,,
|