ht32-code-mcp 0.5.0__tar.gz → 0.5.1__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 (35) hide show
  1. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/DEPLOY.md +1 -1
  2. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/PKG-INFO +1 -1
  3. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/pyproject.toml +1 -1
  4. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/src/ht32_code_mcp/server.py +3 -1
  5. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/src/ht32_code_mcp/usage.py +68 -1
  6. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/tests/test_tools.py +54 -1
  7. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/uv.lock +1 -1
  8. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/.gitignore +0 -0
  9. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/.gitkeep +0 -0
  10. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/.pytest_base_final_run.log +0 -0
  11. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/README.md +0 -0
  12. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/components.example.json +0 -0
  13. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/e2e_scenario_a.py +0 -0
  14. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/e2e_scenario_b.py +0 -0
  15. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/smoke_http.py +0 -0
  16. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/smoke_stdio.py +0 -0
  17. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/src/ht32_code_mcp/__init__.py +0 -0
  18. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/src/ht32_code_mcp/__main__.py +0 -0
  19. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/src/ht32_code_mcp/component_exporter.py +0 -0
  20. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/src/ht32_code_mcp/component_source.py +0 -0
  21. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/src/ht32_code_mcp/components.py +0 -0
  22. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/src/ht32_code_mcp/exporter.py +0 -0
  23. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/src/ht32_code_mcp/index.py +0 -0
  24. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/src/ht32_code_mcp/knowledge.py +0 -0
  25. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/src/ht32_code_mcp/materializer.py +0 -0
  26. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/src/ht32_code_mcp/release.py +0 -0
  27. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/tests/conftest.py +0 -0
  28. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/tests/test_component_export.py +0 -0
  29. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/tests/test_component_redact.py +0 -0
  30. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/tests/test_component_source.py +0 -0
  31. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/tests/test_component_tools.py +0 -0
  32. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/tests/test_components.py +0 -0
  33. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/tests/test_export_cloud_mode.py +0 -0
  34. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/tests/test_knowledge_grounding.py +0 -0
  35. {ht32_code_mcp-0.5.0 → ht32_code_mcp-0.5.1}/tests/test_recipe_claims.py +0 -0
@@ -25,7 +25,7 @@ MCP 与 bundle 都跑在应用机(Linux 本地机),云端 OpenResty 只做
25
25
  ht32-bundles-web 宿主 18082 → 容器 80 /ht32-bundles
26
26
  ```
27
27
 
28
- 反代配置原文在**工作区** `maintenance/openresty/`(`ht32-code.conf` / `ht32-bundles.conf` / `default.conf`),
28
+ 反代配置原文在**工作区独立仓库** `ht32-mcp-maint/openresty/`(`ht32-code.conf` / `ht32-bundles.conf` / `default.conf`),
29
29
  手工贴进 1Panel 站点 `mcp.s621.work` 的同一个 server 块。`/ht32-bundles` **必须反代**
30
30
  到 `100.66.1.6:18082`,且 `proxy_pass` 不能带尾斜杠。
31
31
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ht32-code-mcp
3
- Version: 0.5.0
3
+ Version: 0.5.1
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.5.0"
7
+ version = "0.5.1"
8
8
  description = "HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出"
9
9
  requires-python = ">=3.11"
10
10
  license = "MIT"
@@ -847,7 +847,9 @@ def check_skill_update(current: str = "") -> str:
847
847
  Args:
848
848
  current: 本机已安装的 ht32-skills 版本,取
849
849
  `python "<global-skill-root>\\_ht32-runtime\\ht32.py" version` 的
850
- `skill_version`(如 "v0.1.0");拿不到就留空,报告会说明如何补
850
+ `skill_version`(如 "v0.1.0");拿不到就留空,报告会说明如何补。
851
+ 传入的版本会记到本 MCP 会话,本次会话后续的 usage 记录均带上
852
+ `skill_version`(服务端拿不到客户端版本,只能靠这里带入)
851
853
  """
852
854
  return update_report(CODE_ROOT, current)
853
855
 
@@ -11,6 +11,8 @@
11
11
  - **feedback**(`record_feedback`,由各 server 的 `submit_feedback` 工具调用):
12
12
  记录 Agent 主动上报的语义性问题,middleware 无法判断这些。其中 `skill_version`
13
13
  由 Agent 自报(服务端跨进程拿不到客户端 skill 版本),用于定位 ht32-skills 缺陷版本。
14
+ 同一版本还会按 MCP 会话记住(见 `SKILL_VERSION_ARGS`),该会话后续的 tool_call
15
+ 记录也会带上 `skill_version`;上报之前或无 session 的调用仍为空串。
14
16
 
15
17
  目录布局(同根 + 服务子目录):
16
18
 
@@ -39,7 +41,7 @@ from typing import Any
39
41
  from fastmcp.server.middleware import Middleware, MiddlewareContext
40
42
 
41
43
  # 实现版本号:两仓库副本必须一致(各仓测试会校验,防止只改一边造成漂移)
42
- _IMPL_VERSION = "1.2.0"
44
+ _IMPL_VERSION = "1.3.0"
43
45
 
44
46
  # 服务名 → 子目录名。新增服务在此登记。
45
47
  SERVICE_WIKI = "wiki"
@@ -140,6 +142,64 @@ def _client_info(context: MiddlewareContext) -> dict[str, str]:
140
142
  return out
141
143
 
142
144
 
145
+ # ---- 客户端 skill 版本(会话级)--------------------------------------------
146
+ # usage 在服务端采集,跨进程拿不到客户端 ht32-skills 版本,只能由 Agent 在上报型
147
+ # 工具里带入。这些工具的参数名登记如下:中间件记录调用时顺手把版本记到当前 MCP
148
+ # 会话,该会话**后续所有** tool_call 都补 skill_version 字段。
149
+ SKILL_VERSION_ARGS = {
150
+ "check_skill_update": "current", # code:会话开始查更新(顺带上报版本)
151
+ "submit_feedback": "skill_version", # code / wiki:提交反馈
152
+ "report_skill_version": "version", # wiki:会话开始主动上报
153
+ }
154
+ # 常驻进程会持续接新会话,会话表设上限,超出按插入顺序淘汰最老的
155
+ _MAX_SKILL_SESSIONS = 512
156
+ _SESSION_SKILL_VERSION: dict[str, str] = {}
157
+
158
+
159
+ def _session_key(context: Any) -> str:
160
+ """会话标识(MiddlewareContext 或工具 Context 皆可);取不到返回空串。
161
+
162
+ stdio / in-memory 下 FastMCP 的 session_id 是每请求新生成的 UUID,直接拿来
163
+ 做键会永远 miss;这两种传输一个进程只服务一个客户端,故统一用固定键。
164
+ HTTP 才有稳定的 mcp-session-id,按它隔离多用户会话。
165
+ """
166
+ ctx = getattr(context, "fastmcp_context", None)
167
+ if ctx is None:
168
+ ctx = context
169
+ try:
170
+ transport = str(getattr(ctx, "transport", "") or "")
171
+ except Exception:
172
+ transport = ""
173
+ if transport in ("", "memory", "stdio"):
174
+ return "local"
175
+ for key in ("session_id", "client_id"):
176
+ try:
177
+ val = getattr(ctx, key, None)
178
+ except Exception:
179
+ continue
180
+ if val:
181
+ return f"{key}:{val}"
182
+ return ""
183
+
184
+
185
+ def remember_skill_version(context: Any, version: str) -> None:
186
+ """把客户端上报的 ht32-skills 版本记到当前会话(供 usage 打标)。"""
187
+ ver = (version or "").strip()
188
+ key = _session_key(context)
189
+ if not ver or not key:
190
+ return
191
+ _SESSION_SKILL_VERSION.pop(key, None)
192
+ while len(_SESSION_SKILL_VERSION) >= _MAX_SKILL_SESSIONS:
193
+ _SESSION_SKILL_VERSION.pop(next(iter(_SESSION_SKILL_VERSION)), None)
194
+ _SESSION_SKILL_VERSION[key] = ver
195
+
196
+
197
+ def session_skill_version(context: Any) -> str:
198
+ """当前会话已上报的 ht32-skills 版本;没有则空串。"""
199
+ key = _session_key(context)
200
+ return _SESSION_SKILL_VERSION.get(key, "") if key else ""
201
+
202
+
143
203
  class UsageMiddleware(Middleware):
144
204
  """把所有工具调用落成 JSONL(服务端自动采集)。"""
145
205
 
@@ -156,6 +216,12 @@ class UsageMiddleware(Middleware):
156
216
 
157
217
  args = dict(context.message.arguments or {})
158
218
  recorded = {k: v for k, v in args.items() if k not in ARG_NOISE}
219
+ # 上报型工具:先把版本记到当前会话,本次记录也会带上它
220
+ ver_arg = SKILL_VERSION_ARGS.get(context.message.name)
221
+ if ver_arg:
222
+ val = args.get(ver_arg)
223
+ if isinstance(val, str):
224
+ remember_skill_version(context, val)
159
225
  started = time.perf_counter()
160
226
  error = ""
161
227
  ok = True
@@ -186,6 +252,7 @@ class UsageMiddleware(Middleware):
186
252
  "error": error,
187
253
  "mcp_version": self.mcp_version,
188
254
  "kb_version": self.kb_version,
255
+ "skill_version": session_skill_version(context),
189
256
  **_client_info(context),
190
257
  }
191
258
  write_record(self.log_dir, USAGE_PREFIX, record)
@@ -679,7 +679,7 @@ def test_version_reports_skill_latest(monkeypatch):
679
679
  def test_usage_impl_version_pinned():
680
680
  """两仓库的 usage.py 是逐字节相同副本,实现版本号用于防止只改一边。"""
681
681
  from ht32_code_mcp import usage
682
- assert usage._IMPL_VERSION == "1.2.0"
682
+ assert usage._IMPL_VERSION == "1.3.0"
683
683
  assert usage.SERVICE_CODE == "code"
684
684
  assert usage.SERVICE_WIKI == "wiki"
685
685
  # 噪声参数含 out_dir(code 特有:可能含用户本地路径)
@@ -842,6 +842,59 @@ def test_usage_middleware_records_calls(tmp_path):
842
842
  assert "deliberate" in fail_rec["error"]
843
843
 
844
844
 
845
+ def test_usage_middleware_tags_session_skill_version(tmp_path):
846
+ """上报型工具带来客户端 skill 版本:按会话记住,后续调用都带上;
847
+ 没上报过的会话保持空串(不跨会话串号)。"""
848
+ import asyncio
849
+
850
+ from fastmcp import Client, FastMCP
851
+
852
+ app = FastMCP("mw-skill")
853
+ app.add_middleware(UsageMiddleware(str(tmp_path / "logs"), "code",
854
+ mcp_version="0.5.1", kb_version="v0.4.0"))
855
+
856
+ @app.tool()
857
+ def check_skill_update(current: str = "") -> str:
858
+ return f"current={current}"
859
+
860
+ @app.tool()
861
+ def probe(chip: str) -> str:
862
+ return "ok:" + chip
863
+
864
+ async def run():
865
+ # in-memory 传输按"本进程唯一客户端"归键:上报前 → 上报 → 上报后
866
+ async with Client(app) as c:
867
+ await c.call_tool("probe", {"chip": "before"})
868
+ await c.call_tool("check_skill_update", {"current": "v0.1.34"})
869
+ await c.call_tool("probe", {"chip": "after"})
870
+
871
+ asyncio.run(run())
872
+ recs = [json.loads(l) for l in
873
+ next((tmp_path / "logs").glob("usage_*.jsonl"))
874
+ .read_text(encoding="utf-8").splitlines()]
875
+ assert [r.get("skill_version") for r in recs] == ["", "v0.1.34", "v0.1.34"]
876
+ # 上报调用本身仍保留原始参数
877
+ assert recs[1]["args"] == {"current": "v0.1.34"}
878
+
879
+
880
+ def test_skill_version_isolated_per_http_session():
881
+ """HTTP 传输按 session_id 隔离:A 上报的版本不会串到 B 的会话。"""
882
+ from types import SimpleNamespace
883
+
884
+ from ht32_code_mcp.usage import remember_skill_version, session_skill_version
885
+
886
+ def fake_ctx(sid: str):
887
+ return SimpleNamespace(fastmcp_context=SimpleNamespace(
888
+ transport="streamable-http", session_id=sid, client_id=None))
889
+
890
+ remember_skill_version(fake_ctx("sess-a"), "v0.1.34")
891
+ assert session_skill_version(fake_ctx("sess-a")) == "v0.1.34"
892
+ assert session_skill_version(fake_ctx("sess-b")) == ""
893
+ # 空版本 / 无会话不落表
894
+ remember_skill_version(fake_ctx("sess-b"), " ")
895
+ assert session_skill_version(fake_ctx("sess-b")) == ""
896
+
897
+
845
898
  def test_usage_middleware_records_soft_failure(tmp_path):
846
899
  """工具返回错误字符串(不抛异常)时,middleware 应记 ok=false 并留错误原文。"""
847
900
  import asyncio
@@ -440,7 +440,7 @@ wheels = [
440
440
 
441
441
  [[package]]
442
442
  name = "ht32-code-mcp"
443
- version = "0.5.0"
443
+ version = "0.5.1"
444
444
  source = { editable = "." }
445
445
  dependencies = [
446
446
  { name = "fastmcp" },
File without changes
File without changes
File without changes