ht32-code-mcp 0.5.1__tar.gz → 0.5.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 (35) hide show
  1. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/DEPLOY.md +7 -8
  2. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/PKG-INFO +1 -1
  3. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/README.md +2 -2
  4. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/pyproject.toml +1 -1
  5. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/src/ht32_code_mcp/usage.py +31 -5
  6. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/tests/test_tools.py +34 -1
  7. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/uv.lock +1 -1
  8. ht32_code_mcp-0.5.1/.pytest_base_final_run.log +0 -4
  9. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/.gitignore +0 -0
  10. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/.gitkeep +0 -0
  11. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/components.example.json +0 -0
  12. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/e2e_scenario_a.py +0 -0
  13. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/e2e_scenario_b.py +0 -0
  14. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/smoke_http.py +0 -0
  15. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/smoke_stdio.py +0 -0
  16. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/src/ht32_code_mcp/__init__.py +0 -0
  17. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/src/ht32_code_mcp/__main__.py +0 -0
  18. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/src/ht32_code_mcp/component_exporter.py +0 -0
  19. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/src/ht32_code_mcp/component_source.py +0 -0
  20. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/src/ht32_code_mcp/components.py +0 -0
  21. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/src/ht32_code_mcp/exporter.py +0 -0
  22. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/src/ht32_code_mcp/index.py +0 -0
  23. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/src/ht32_code_mcp/knowledge.py +0 -0
  24. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/src/ht32_code_mcp/materializer.py +0 -0
  25. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/src/ht32_code_mcp/release.py +0 -0
  26. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/src/ht32_code_mcp/server.py +0 -0
  27. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/tests/conftest.py +0 -0
  28. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/tests/test_component_export.py +0 -0
  29. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/tests/test_component_redact.py +0 -0
  30. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/tests/test_component_source.py +0 -0
  31. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/tests/test_component_tools.py +0 -0
  32. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/tests/test_components.py +0 -0
  33. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/tests/test_export_cloud_mode.py +0 -0
  34. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/tests/test_knowledge_grounding.py +0 -0
  35. {ht32_code_mcp-0.5.1 → ht32_code_mcp-0.5.2}/tests/test_recipe_claims.py +0 -0
@@ -42,15 +42,14 @@ PyPI 包 → FastMCP 直接提供 Streamable HTTP(常驻单进程)
42
42
 
43
43
  ### 镜像与端点
44
44
 
45
- - 镜像 `ht32-code-mcp:0.5.0`:`ghcr.io/astral-sh/uv:python3.12-bookworm-slim` +
46
- `git/curl/ca-certificates`,`uv tool install ht32-code-mcp==0.5.0`,入口直跑
45
+ - 镜像 `ht32-code-mcp:0.5.1`:`ghcr.io/astral-sh/uv:python3.12-bookworm-slim` +
46
+ `git/curl/ca-certificates`,`uv tool install ht32-code-mcp==0.5.1`,入口直跑
47
47
  `ht32-code-mcp`(私有组件能力仍需服务端 git)。由 `.work/docker/Dockerfile` 构建。
48
48
  - 传输:生产 `MCP_TRANSPORT=streamable-http`,`path=/ht32-code`(`MCP_PATH` 注入);
49
49
  本地调试保持默认 `/mcp`。`stdio` 仅供冒烟测试。
50
50
  - `MCP_JSON_RESPONSE=0`:保持 SSE,兼容现有客户端。
51
51
  - 健康检查:`GET /health` → `{"status":"ok","version":"<包版本>"}`,不触发检索。
52
- - 资源:`mem_limit: 1g` / `cpus: 1.0` / `pids_limit: 512`(常驻单进程与多会话对象);
53
- 不再使用 V1 按「一会话一进程」算出的 `4g` / `1024` 熔断值。
52
+ - 资源:`mem_limit: 1g` / `cpus: 1.0` / `pids_limit: 512`(常驻单进程与多会话对象)。
54
53
 
55
54
  ## compose 关键配置
56
55
 
@@ -66,7 +65,7 @@ PyPI 包 → FastMCP 直接提供 Streamable HTTP(常驻单进程)
66
65
  | `COMPONENTS_CONFIG` | `/app/code/.work/components/components.json` |
67
66
  | `COMPONENTS_STATE_FILE` | `/app/feedback/component-tag-lock.json` |
68
67
  | 凭据 | `.work/docker/.env` 的 `COMPONENTS_GIT_AUTH`(base64 用户名:只读 token),已 gitignore |
69
- | `MCP_TRANSPORT` | `streamable-http`(不经 supergateway) |
68
+ | `MCP_TRANSPORT` | `streamable-http`(容器直跑,无中间网关) |
70
69
  | `MCP_HOST` / `MCP_PORT` | `0.0.0.0` / `8001` |
71
70
  | `MCP_PATH` | `/ht32-code`(须与反代 location 一致) |
72
71
  | `MCP_STATELESS_HTTP` | `0`(P0 保留进程内有状态会话) |
@@ -140,7 +139,7 @@ location ^~ /ht32-bundles {
140
139
 
141
140
  | 通道 | 文件 | 触发 | 内容 |
142
141
  |------|------|------|------|
143
- | 用量日志 | `usage_*.jsonl` | 服务端自动(middleware) | 工具名、参数原文、耗时、成败、session |
142
+ | 用量日志 | `usage_*.jsonl` | 服务端自动(middleware) | 工具名、参数原文、耗时、成败、session、客户端身份(`client_name`/`client_version`,取自 initialize 的 `clientInfo`) |
144
143
  | 反馈 | `feedback_*.jsonl` | Agent 主动(`submit_feedback`) | 语义性问题 |
145
144
 
146
145
  - `FEEDBACK_ROOT` 为空 = 整体关闭(本地开发默认不落盘);必须填**绝对路径**。
@@ -272,8 +271,8 @@ curl -s -o /dev/null -w "%{http_code}" https://mcp.s621.work/ht32-code # 400 =
272
271
  ```
273
272
 
274
273
  - 启动日志应含 `启动服务: Streamable HTTP 0.0.0.0:8001/ht32-code(stateless=False, json=False)`;
275
- 若仍见 supergateway / `--stateful`,说明服务器 compose 未同步或镜像未重建
276
- - 协议版本被拒 → 旧镜像或旧客户端;本版直接由 FastMCP 4.x 提供,不再经 supergateway
274
+ 否则检查服务器 compose 与镜像是否已同步
275
+ - 协议版本被拒 → 检查镜像版本与客户端协议版本
277
276
  - `/health` 404 → 进程是旧版(`MCP_PATH` 之前的版本没有该路由)
278
277
  - 容器 126 退出 → 旧 compose 的 `$$(...)` 命令替换被吃掉;新版无 `command`,重新 `git pull` 即可
279
278
  - 首会话超时 → 冷启动(索引加载);容器健康检查 `start_period: 60s` 已留余量
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ht32-code-mcp
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出
5
5
  License-Expression: MIT
6
6
  Keywords: firmware,holtek,ht32,mcp,mcu
@@ -1,8 +1,8 @@
1
1
  # .work/mcp-server/
2
2
 
3
- MCP server 代码目录(Python + FastMCP),实现 11 个检索/导出工具 + 版本检查 + 1 个反馈工具。
3
+ MCP server 代码目录(Python + FastMCP),实现 **13 个工具**(检索/导出/组件/版本检查/反馈)。
4
4
 
5
- 传输由 `MCP_TRANSPORT` 决定:`stdio`(默认,供 supergateway 包装)/ `streamable-http` / `sse`。
5
+ 传输由 `MCP_TRANSPORT` 决定:`stdio`(本地调试/冒烟)/ `streamable-http`(线上,FastMCP 常驻单进程)/ `sse`(旧客户端兼容)。
6
6
  部署与排错见 [DEPLOY.md](DEPLOY.md)。
7
7
 
8
8
  ## 私有组件(0.4.5+)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ht32-code-mcp"
7
- version = "0.5.1"
7
+ version = "0.5.2"
8
8
  description = "HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出"
9
9
  requires-python = ">=3.11"
10
10
  license = "MIT"
@@ -7,7 +7,9 @@
7
7
  两条采集通道,写入同一目录树:
8
8
 
9
9
  - **tool_call**(`UsageMiddleware`,服务端自动):拦截所有工具调用,
10
- 记录工具名、参数原文、耗时、成败。不依赖 Agent 配合,数据完整。
10
+ 记录工具名、参数原文、耗时、成败,以及 MCP `initialize` 请求里的客户端身份
11
+ (`client_info.name` / `client_info.version` → 日志字段 `client_name` /
12
+ `client_version`,老客户端未上报时为空串)。不依赖 Agent 配合,数据完整。
11
13
  - **feedback**(`record_feedback`,由各 server 的 `submit_feedback` 工具调用):
12
14
  记录 Agent 主动上报的语义性问题,middleware 无法判断这些。其中 `skill_version`
13
15
  由 Agent 自报(服务端跨进程拿不到客户端 skill 版本),用于定位 ht32-skills 缺陷版本。
@@ -41,7 +43,7 @@ from typing import Any
41
43
  from fastmcp.server.middleware import Middleware, MiddlewareContext
42
44
 
43
45
  # 实现版本号:两仓库副本必须一致(各仓测试会校验,防止只改一边造成漂移)
44
- _IMPL_VERSION = "1.3.0"
46
+ _IMPL_VERSION = "1.4.0"
45
47
 
46
48
  # 服务名 → 子目录名。新增服务在此登记。
47
49
  SERVICE_WIKI = "wiki"
@@ -126,12 +128,35 @@ def write_record(log_dir: str, prefix: str, record: dict[str, Any]) -> Path | No
126
128
  return None
127
129
 
128
130
 
131
+ def _initialize_client(ctx: Any) -> tuple[str, str]:
132
+ """从 MCP `initialize` 的 `clientInfo` 取客户端名与版本。
133
+
134
+ FastMCP 4 / MCP SDK 2 把 initialize 参数常驻在会话底层 Connection 上
135
+ (`session.client_params.client_info`),initialize 时写一次、整段会话只读,
136
+ 因此工具调用阶段可直接读取,无需另建缓存,也就不存在并发写竞态。
137
+ 取不到(无会话 / 客户端未上报 / 字段缺失 / 访问抛错)一律返回空串。
138
+ """
139
+ try:
140
+ session = getattr(ctx, "session", None)
141
+ params = getattr(session, "client_params", None) if session is not None else None
142
+ info = getattr(params, "client_info", None) if params is not None else None
143
+ if info is None:
144
+ return "", ""
145
+ return str(getattr(info, "name", "") or ""), str(getattr(info, "version", "") or "")
146
+ except Exception: # noqa: BLE001 - 采集失败绝不能影响工具调用本身
147
+ return "", ""
148
+
149
+
129
150
  def _client_info(context: MiddlewareContext) -> dict[str, str]:
130
- """取客户端标识(内存调用等无 session 的场景返回空值)。"""
151
+ """取客户端标识(内存调用等无 session 的场景返回空值)。
152
+
153
+ `client_name` / `client_version` 恒定存在:老客户端未上报 clientInfo 时为空串,
154
+ 保证日志行字段结构不变形。
155
+ """
156
+ out: dict[str, str] = {"client_name": "", "client_version": ""}
131
157
  ctx = getattr(context, "fastmcp_context", None)
132
158
  if ctx is None:
133
- return {}
134
- out: dict[str, str] = {}
159
+ return out
135
160
  for key in ("client_id", "session_id"):
136
161
  val = getattr(ctx, key, None)
137
162
  if val:
@@ -139,6 +164,7 @@ def _client_info(context: MiddlewareContext) -> dict[str, str]:
139
164
  transport = getattr(ctx, "transport", None)
140
165
  if transport:
141
166
  out["transport"] = str(transport)
167
+ out["client_name"], out["client_version"] = _initialize_client(ctx)
142
168
  return out
143
169
 
144
170
 
@@ -679,13 +679,43 @@ 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.3.0"
682
+ assert usage._IMPL_VERSION == "1.4.0"
683
683
  assert usage.SERVICE_CODE == "code"
684
684
  assert usage.SERVICE_WIKI == "wiki"
685
685
  # 噪声参数含 out_dir(code 特有:可能含用户本地路径)
686
686
  assert {"limit", "max_length", "out_dir"} <= usage.ARG_NOISE
687
687
 
688
688
 
689
+ def test_usage_client_info_from_initialize():
690
+ """initialize 的 clientInfo 落到 client_name/client_version;缺失时不报错且为空串。"""
691
+ from types import SimpleNamespace
692
+
693
+ from fastmcp.server.middleware import MiddlewareContext
694
+
695
+ from ht32_code_mcp.usage import _client_info
696
+
697
+ def fake_ctx(**kwargs):
698
+ return MiddlewareContext(message=None, fastmcp_context=SimpleNamespace(**kwargs))
699
+
700
+ with_info = fake_ctx(
701
+ transport="streamable-http", session_id="s1", client_id=None,
702
+ session=SimpleNamespace(client_params=SimpleNamespace(
703
+ client_info=SimpleNamespace(name="Trae", version="3.4.15"))))
704
+ info = _client_info(with_info)
705
+ assert info["client_name"] == "Trae"
706
+ assert info["client_version"] == "3.4.15"
707
+
708
+ # 老客户端 / 现代协议未带 clientInfo:字段仍在,值为空串,且不抛错
709
+ for no_info in (
710
+ fake_ctx(transport="stdio", session_id="s2", client_id=None,
711
+ session=SimpleNamespace(client_params=None)),
712
+ fake_ctx(transport="stdio", session_id="s3", client_id=None,
713
+ session=SimpleNamespace(client_params=SimpleNamespace(client_info=None))),
714
+ ):
715
+ out = _client_info(no_info)
716
+ assert out["client_name"] == "" and out["client_version"] == ""
717
+
718
+
689
719
  def test_soft_failure_marker_semantics():
690
720
  """软失败判定口径 = 工具是否完成工作。
691
721
 
@@ -838,6 +868,9 @@ def test_usage_middleware_records_calls(tmp_path):
838
868
  assert ok_rec["duration_ms"] >= 0
839
869
  assert ok_rec["mcp_version"] == "0.1.4"
840
870
  assert ok_rec["kb_version"] == "v0.1.0"
871
+ # 客户端身份字段恒定存在(内存客户端可能未上报 clientInfo → 空串)
872
+ assert isinstance(ok_rec["client_name"], str)
873
+ assert isinstance(ok_rec["client_version"], str)
841
874
  assert fail_rec["tool"] == "boom" and fail_rec["ok"] is False
842
875
  assert "deliberate" in fail_rec["error"]
843
876
 
@@ -440,7 +440,7 @@ wheels = [
440
440
 
441
441
  [[package]]
442
442
  name = "ht32-code-mcp"
443
- version = "0.5.1"
443
+ version = "0.5.2"
444
444
  source = { editable = "." }
445
445
  dependencies = [
446
446
  { name = "fastmcp" },
@@ -1,4 +0,0 @@
1
- ........................................................................ [ 93%]
2
- ..... [100%]
3
- 77 passed in 271.49s (0:04:31)
4
- EXIT=0
File without changes
File without changes