ht32-code-mcp 0.2.1__tar.gz → 0.2.3__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 (23) hide show
  1. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/DEPLOY.md +372 -314
  2. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/PKG-INFO +1 -1
  3. ht32_code_mcp-0.2.3/README.md +6 -0
  4. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/pyproject.toml +35 -35
  5. ht32_code_mcp-0.2.3/smoke_http.py +193 -0
  6. ht32_code_mcp-0.2.3/src/ht32_code_mcp/__init__.py +4 -0
  7. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/src/ht32_code_mcp/exporter.py +128 -70
  8. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/src/ht32_code_mcp/index.py +159 -47
  9. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/src/ht32_code_mcp/server.py +547 -448
  10. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/tests/test_tools.py +336 -8
  11. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/uv.lock +1 -1
  12. ht32_code_mcp-0.2.1/README.md +0 -5
  13. ht32_code_mcp-0.2.1/ht32-code-mcp-update.md +0 -143
  14. ht32_code_mcp-0.2.1/src/ht32_code_mcp/__init__.py +0 -3
  15. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/.gitignore +0 -0
  16. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/.gitkeep +0 -0
  17. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/e2e_scenario_a.py +0 -0
  18. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/e2e_scenario_b.py +0 -0
  19. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/smoke_stdio.py +0 -0
  20. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/src/ht32_code_mcp/__main__.py +0 -0
  21. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/src/ht32_code_mcp/knowledge.py +0 -0
  22. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/src/ht32_code_mcp/usage.py +0 -0
  23. {ht32_code_mcp-0.2.1 → ht32_code_mcp-0.2.3}/tests/conftest.py +0 -0
@@ -1,314 +1,372 @@
1
- # MCP Server 部署指南(ht32-code)
2
-
3
- HT32 Code MCP Server 的部署、配置与排错。代码在 `.work/mcp-server/`,已发布到 PyPI(`ht32-code-mcp`)。
4
-
5
- ## 部署信息
6
-
7
- ```
8
- MCP Server 地址(Streamable HTTP): https://mcp.s621.work/ht32-code
9
- PyPI 包: https://pypi.org/project/ht32-code-mcp/(uvx ht32-code-mcp 即可运行)
10
- Bundle 下载地址: https://mcp.s621.work/ht32-bundles/<token>.zip
11
- 本地 Gitea: http://100.66.1.6:3000/S621/ht32-code.git
12
- MCP Server 代码: .work/mcp-server/
13
- ```
14
-
15
- ## 部署链路
16
-
17
- ```
18
- PyPI 发布包 → 1Panel 原生 MCP 管理(类型 uvx,启动命令 uvx ht32-code-mcp,输出类型 streamableHttp)
19
- → supergateway 包装 stdio 为 Streamable HTTP(stateful)
20
- → OpenResty 暴露公网
21
- ├─ /ht32-code/* → MCP 协议端点(反向代理到容器)
22
- └─ /ht32-bundles/* → bundle zip 静态目录(P1-3)
23
- ```
24
-
25
- 代码默认走 `stdio`(`MCP_TRANSPORT` 未设置时),supergateway 负责 stdio→Streamable HTTP 转换。
26
- 与 ht32-wiki 同构:SSE 无心跳会被 OpenResty 空闲切断,故走 streamableHttp stateful。
27
-
28
- ## 关键配置
29
-
30
- 1Panel MCP 表单:
31
-
32
- - 类型:`uvx`
33
- - 启动命令:`uvx ht32-code-mcp`(**必须带 uvx 前缀**,容器里没装裸命令)
34
- - 输出类型:`streamableHttp`
35
- - 流式传输路径:`/ht32-code`(切输出类型后是**新字段**,默认空,必须手填;必填校验)
36
- - 协议版本:`2025-06-18`(1Panel 预填默认值,勿动;仅用于 supergateway 对下游 stdio 的 auto-init)
37
- - 镜像:`supercorp/supergateway:next-uvx`(**必须覆盖默认的 `:uvx`**——见下方踩坑)
38
- - 参数:`--stateful --sessionTimeout 3600000`
39
- - 环境变量:`CODE_ROOT=/app/code`
40
- - 挂载:`/opt/ht32-code:/app/code`(**不用加 `:ro`**——`index/code_index.json`
41
- 已 gitignore 且容器内可能需重建,只读会阻断;容器路径不能有前导空格,否则 `invalid mount path`)
42
-
43
- ### 镜像 tag 踩坑(2026-09-20)
44
-
45
- 默认镜像 `supercorp/supergateway:uvx` 构建时解析到旧版
46
- `@modelcontextprotocol/sdk`(LATEST_PROTOCOL_VERSION=2025-06-18),新版客户端
47
- (Trae 发 `2025-11-25`)initialize 直接被拒:`Bad Request: Unsupported protocol
48
- version`。根因是 supergateway 3.4.3 对 SDK 声明的是浮动依赖 `^1.18.2`,镜像构建
49
- 日期决定行为;`next-uvx`(4.0.0-rc.4)将 SDK **精确锁定 1.30.0**,无此问题。
50
-
51
- ## 云端 zip 下载通道(P1-3,0.1.2+)
52
-
53
- `export_project_bundle` 有两种交付形态,**由服务端环境变量决定,调用方无需判断**:
54
-
55
- | 模式 | 触发条件 | 行为 |
56
- |---|---|---|
57
- | 云端 zip | `BUNDLE_DIR` 与 `BUNDLE_BASE_URL` **同时非空** | 打包到 `BUNDLE_DIR`,返回 `<BUNDLE_BASE_URL>/<token>.zip` + SHA256 + 过期时间 |
58
- | 本地目录 | 二者任一为空(默认) | 物化到 `out_dir`(须为空目录/不存在) |
59
-
60
- ### 环境变量
61
-
62
- | 变量 | 默认 | 说明 |
63
- |------|------|------|
64
- | `BUNDLE_DIR` | 空(= 关闭云端模式) | 容器内 bundle 输出目录,须可写 |
65
- | `BUNDLE_BASE_URL` | 空(= 关闭云端模式) | 公网下载前缀,如 `https://mcp.s621.work/ht32-bundles` |
66
- | `BUNDLE_TTL_SECONDS` | `3600` | zip 有效期;每次 export 前顺带清理过期 zip |
67
-
68
- 非数字的 `BUNDLE_TTL_SECONDS` 会静默回退默认值,不报错。
69
-
70
- ### 实现要点
71
-
72
- - **token**:`secrets.token_urlsafe(24)`(192 bit 熵),文件名即 token;不可枚举、不可猜测
73
- - **物化在系统临时目录**(`tempfile.mkdtemp`)而非 bundle 目录——bundle 目录里只出现
74
- 最终 zip,中途失败的半成品不会暴露成可下载文件
75
- - **清理**:`sweep_expired()` 在每次 export 时按 mtime 删除超期 `*.zip`;
76
- 进程崩溃遗留的临时目录不影响交付(不在 bundle 目录内)
77
- - **TTL 到点不主动删**:靠下次 export 的 sweep 或 P1-2 定时脚本清理;过期后
78
- 重新调用工具即可拿新 URL
79
-
80
- ### 服务器侧落地(3 步)
81
-
82
- 本部署的 OpenResty 是**容器化**的(容器名 `1Panel-openresty-ih7J`),
83
- 已挂载 `WEBSITE_DIR=/opt/1panel/www` → 容器内 `/www`。
84
- 因此 bundle 目录直接落在网站根目录树下,**OpenResty 的 volumes 一行都不用改**——
85
- 两个容器通过宿主机上的同一目录交换文件:
86
-
87
- ```
88
- 宿主机 /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles/ (站点 root 目录下 + /ht32-bundles)
89
- ├─ code-mcp 容器看到 /app/bundles
90
- └─ OpenResty 容器看到 /www/sites/mcp.s621.work/index/ht32-bundles
91
- 公网 URL https://mcp.s621.work/ht32-bundles/<token>.zip
92
- ```
93
-
94
- 站点「网站目录」显示 root = `/opt/1panel/www/sites/mcp.s621.work/index`,
95
- bundle 放其下子目录即被当作静态文件直接访问。
96
-
97
- **第 1 步:宿主机建 bundle 目录**
98
-
99
- ```bash
100
- mkdir -p /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles
101
- chmod 755 /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles
102
- ```
103
-
104
- **第 2 步:1Panel 给 code-mcp 容器补挂载与环境变量**
105
-
106
- | 配置项 | 值 |
107
- |--------|-----|
108
- | 挂载 | `/opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles:/app/bundles`(**不要加 `:ro`,需可写**) |
109
- | 环境变量 | `BUNDLE_DIR=/app/bundles` |
110
- | 环境变量 | `BUNDLE_BASE_URL=https://mcp.s621.work/ht32-bundles` |
111
- | 环境变量 | `BUNDLE_TTL_SECONDS=3600`(可选,默认即 3600) |
112
-
113
- 完整配置(含现有项):
114
-
115
- ```
116
- 类型: uvx
117
- 启动命令: uvx ht32-code-mcp
118
- 输出类型: streamableHttp
119
- 流式传输路径:/ht32-code
120
- 协议版本: 2025-06-18
121
- 镜像: supercorp/supergateway:next-uvx
122
- 参数: --stateful --sessionTimeout 3600000
123
- 环境变量: CODE_ROOT=/app/code
124
- BUNDLE_DIR=/app/bundles
125
- BUNDLE_BASE_URL=https://mcp.s621.work/ht32-bundles
126
- 挂载: /opt/ht32-code:/app/code
127
- /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles:/app/bundles ← 宿主机路径,非容器内路径
128
- ```
129
-
130
- **第 3 步:OpenResty 无需任何配置**
131
-
132
- 站点 root = `/opt/1panel/www/sites/mcp.s621.work/index`,映射到容器内
133
- `/www/sites/mcp.s621.work/index`。bundle 目录落在这个 root 之下子目录,
134
- 静态文件天然可访问,**不用建反向代理、不用手写 location、不用改 compose**。
135
-
136
- > **不要为 bundles 建反向代理条目**:1Panel 反代 UI 表达不了静态 location
137
- > (后端代理地址必填,而静态目录没有后端)。若建了且后端留空,1Panel 生成的
138
- > location 会抢走 `/ht32-bundles/` 却不知从哪读文件 → 404。已有此类条目请删除。
139
-
140
- > **静态 location 无需 `proxy_*` 指令**:文件由 nginx 读盘直接返回,不用
141
- > `proxy_pass`,也不用 `proxy_read_timeout`——这与 code 的代理 location 形态不同属正常。
142
-
143
- > **权限**:1Panel 站点要求 `index` 目录下所有文件/目录属主与属组为 `1000`(见
144
- > 「网站目录」页提示,PHP 容器环境)。nginx worker 需可读该目录;zip 文件 644 /
145
- > 目录 755 正常可读。若验证时拿 403,先查属主属组与 SELinux 上下文。
146
-
147
- 重载(改了容器挂载或环境变量时需重建 code-mcp;仅改文件无需重载 nginx):
148
-
149
- ```bash
150
- docker exec 1Panel-openresty-ih7J nginx -t
151
- ```
152
-
153
- ### 验证生效
154
-
155
- 1. 调 `get_code_version`,应出现:
156
-
157
- ```
158
- 云端 zip 下载:启用(TTL 3600s,https://mcp.s621.work/ht32-bundles)
159
- ```
160
-
161
- 仍显示「未配置」→ 环境变量没注入成功(查变量名 / 容器是否重建)。
162
-
163
- 2. 调 `export_project_bundle(chip="HT32F52352")`,应返回:
164
-
165
- ```
166
- 下载 URL:https://mcp.s621.work/ht32-bundles/<token>.zip
167
- 有效期:3600 秒(至 ...)
168
- 大小:1.53 MB(455 文件)
169
- SHA256:...
170
- ```
171
-
172
- 3. 浏览器/curl 拉该 URL:应得约 1.5 MB zip;`sha256sum` 与报告一致;
173
- 解压后 455 文件、`project/MDK_ARMv5/ht32f5-template.uvprojx` 在,UV4 编译 0E0W。
174
-
175
- 4. 确认 `ls /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles` 只有 `*.zip`(无残留临时目录)。
176
-
177
- ## 反馈与用量日志(P1-5,0.2.0+)
178
-
179
- > **与 ht32-wiki-mcp 统一**:两服务使用同一套实现(`usage.py`,两仓库逐字节
180
- > 相同的副本)、同一套目录约定(同根 + 服务子目录)、同一套字段。
181
- > 本服务写 `<FEEDBACK_ROOT>/code/`,wiki 写 `<FEEDBACK_ROOT>/wiki/`。
182
-
183
- 两条通道,按 UTC 日期分文件(JSON Lines):
184
-
185
- | 通道 | 文件 | 触发 | 记录内容 |
186
- |------|------|------|----------|
187
- | 用量日志 | `usage_*.jsonl` | **服务端自动**(middleware 拦截所有工具调用) | 工具名、参数原文(剔除 `limit`/`max_length`/`out_dir`)、耗时、成败、错误、客户端 session |
188
- | 反馈 | `feedback_*.jsonl` | **Agent 主动**(`submit_feedback` 工具) | 语义性问题:API 错 / 缺口 / 路径错 / 模板缺陷 |
189
-
190
- 自动通道保证调用事实完整(不依赖 Agent 配合),用于追踪用户需求与生成的模板;
191
- 反馈通道补 middleware 判断不了的语义问题。每条记录都带 `service` 字段
192
- (`code`),便于两服务日志合并分析。
193
-
194
- ### 目录布局
195
-
196
- ```
197
- /opt/ht32-mcp-feedback/ ← 宿主机单一入口,两个容器挂同一卷
198
- ├─ wiki/ ← ht32-wiki 容器写这里
199
- └─ code/ ← ht32-code 容器写这里(本服务)
200
- ├─ usage_2026-09-21.jsonl
201
- └─ feedback_2026-09-21.jsonl
202
- ```
203
-
204
- ### 环境变量
205
-
206
- | 变量 | 默认 | 说明 |
207
- |------|------|------|
208
- | `FEEDBACK_ROOT` | 空(= **关闭**,本地开发默认不落盘) | 日志根目录,服务名子目录自动拼接 |
209
- | `CODE_LOG_DIR` | — | 已废弃,改用 `FEEDBACK_ROOT` |
210
-
211
- > **隐私**:参数按原文记录,`query` / `question` 可能含用户项目信息。
212
- > 该目录只应挂在受控服务器上,**不要提交进仓库**(定时脚本是
213
- > `git reset --hard` 镜像模式,写进仓库也会被覆盖丢失)。
214
-
215
- ### 服务器侧落地
216
-
217
- **第 1 步:宿主机建目录**
218
-
219
- ```bash
220
- mkdir -p /opt/ht32-mcp-feedback
221
- chmod 755 /opt/ht32-mcp-feedback
222
- ```
223
-
224
- **第 2 步:两个 MCP 容器都挂同一卷**
225
-
226
- | 服务 | 挂载 | 环境变量 |
227
- |------|------|----------|
228
- | ht32-wiki-mcp | `/opt/ht32-mcp-feedback:/app/feedback` | `FEEDBACK_ROOT=/app/feedback` |
229
- | ht32-code-mcp | `/opt/ht32-mcp-feedback:/app/feedback` | `FEEDBACK_ROOT=/app/feedback` |
230
-
231
- 完整配置(code 侧,含现有项):
232
-
233
- ```
234
- 类型: uvx
235
- 启动命令: uvx ht32-code-mcp
236
- 输出类型: streamableHttp
237
- 流式传输路径:/ht32-code
238
- 协议版本: 2025-06-18
239
- 镜像: supercorp/supergateway:next-uvx
240
- 参数: --stateful --sessionTimeout 3600000
241
- 环境变量: CODE_ROOT=/app/code
242
- BUNDLE_DIR=/app/bundles
243
- BUNDLE_BASE_URL=https://mcp.s621.work/ht32-bundles
244
- FEEDBACK_ROOT=/app/feedback
245
- 挂载: /opt/ht32-code:/app/code
246
- /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles:/app/bundles
247
- /opt/ht32-mcp-feedback:/app/feedback
248
- ```
249
-
250
- ### 验证
251
-
252
- 1. 调 `get_code_version`,应出现
253
- `反馈与用量日志:启用(/app/feedback/code)`。
254
- 仍显示「未配置」→ 环境变量没注入(查变量名 / 容器是否重建)。
255
-
256
- 2. 随便调几个工具,然后看日志:
257
-
258
- ```bash
259
- tail -3 /opt/ht32-mcp-feedback/code/usage_$(date -u +%Y-%m-%d).jsonl
260
- ```
261
-
262
- 3. 统计被请求的芯片型号(指导 P2-1 排优先级)——用 ht32-skills 的分析工具:
263
-
264
- ```bash
265
- # 克隆 ht32-skills 后(或用本地副本)
266
- python ht32-skills/scripts/analyze_feedback.py /opt/ht32-mcp-feedback
267
- ```
268
-
269
- 该工具(零依赖,仅标准库)合并 wiki + code 两服务日志,输出需求信号、
270
- 调用分布与失败、反馈类型、耗时 p50/p95、最近失败与反馈明细;支持
271
- `--days N` / `--service` / `--json`。详见 ht32-skills 的 README。
272
-
273
- 4. 清理策略由定时同步脚本第 7 步负责(`LOG_KEEP_DAYS=30`,与同步日志同口径,
274
- 目录变量为 `FEEDBACK_ROOT`)。
275
-
276
- ## PyPI 包发布流程
277
-
278
- ```bash
279
- # token 在 ht32-code 根目录 .env 的 UV_PUBLISH_TOKEN
280
- uv lock && uv build && uv publish
281
- ```
282
-
283
- 发布后同步 `VERSION` 文件末行:`MCP Server: <版本>(deployed, PyPI, <日期>)`。
284
-
285
- ## 踩过的坑
286
-
287
- ### 1. 云端跑的是 PyPI 包,不是 /opt 仓库(P0-1)
288
-
289
- - **症状**:改了仓库代码、云端 MCP 行为不变
290
- - **根因**:`uvx ht32-code-mcp` 从 PyPI 拉包,`/opt/ht32-code` 只是被检索的数据源
291
- - **修复**:改代码必须**发版 + 重建容器**,改仓库文件本身不生效
292
-
293
- ### 2. 索引只读挂载
294
-
295
- - **症状**:容器启动报索引无法写入/重建失败
296
- - **根因**:`/opt/ht32-code:/app/code:ro` 时 `index/code_index.json` 不可写
297
- - **修复**:挂载去掉 `:ro`
298
-
299
- ### 3. 镜像 tag 导致协议版本被拒
300
-
301
- 见上方「镜像 tag 踩坑」。
302
-
303
- ## 当前部署状态
304
-
305
- | # | 任务 | 说明 | 状态 |
306
- |---|------|------|------|
307
- | 3.1 | MCP 工具实现(7 检索/导出 + 1 反馈) | `.work/mcp-server/src/ht32_code_mcp/`(FastMCP) | ✅ |
308
- | 3.2 | 工具级测试 | `pytest` 44 用例全绿 | ✅ |
309
- | 3.3 | PyPI 发布(hatchling) | `ht32-code-mcp` 0.2.0(2026-09-21,含 bundle 通道 + 统一反馈/用量日志) | ✅ |
310
- | 3.4 | 云端 git clone + 挂载 | `/opt/ht32-code:/app/code` | ✅ |
311
- | 3.5 | 1Panel 部署(uvx + supergateway → streamableHttp stateful,镜像 next-uvx) | `https://mcp.s621.work/ht32-code` | ✅ |
312
- | 3.6 | OpenResty 超时修复(`proxy_read_timeout 3600s`) | 修复 60s 空闲断连;2026-09-21 复核两端空闲 100s 后调用仍 200,未被 1Panel 回退 | ✅ |
313
- | 3.7 | 云端 zip 下载通道(P1-3) | 服务器侧已上线:下载 1608157 字节 / SHA256 一致 / 解压 455 文件 / UV4 0E0W | ✅ |
314
- | 3.8 | 定时同步脚本含 bundle 清理(P1-2) | `ht32-code-mcp-update.md` 第 6 步 `find … -mmin +60 -delete` | ✅ |
1
+ # MCP Server 部署指南(ht32-code)
2
+
3
+ HT32 Code MCP Server 的部署、配置与排错。代码在 `.work/mcp-server/`,已发布到 PyPI(`ht32-code-mcp`)。
4
+
5
+ ## 部署信息
6
+
7
+ ```
8
+ MCP Server 地址(Streamable HTTP): https://mcp.s621.work/ht32-code
9
+ PyPI 包: https://pypi.org/project/ht32-code-mcp/(uvx ht32-code-mcp 即可运行)
10
+ Bundle 下载地址: https://mcp.s621.work/ht32-bundles/<token>.zip
11
+ 本地 Gitea: http://100.66.1.6:3000/S621/ht32-code.git
12
+ MCP Server 代码: .work/mcp-server/
13
+ ```
14
+
15
+ ## 部署链路
16
+
17
+ ```
18
+ PyPI 发布包 → 1Panel 原生 MCP 管理(类型 uvx,启动命令 uvx ht32-code-mcp,输出类型 streamableHttp)
19
+ → supergateway 包装 stdio 为 Streamable HTTP(stateful)
20
+ → OpenResty 暴露公网
21
+ ├─ /ht32-code/* → MCP 协议端点(反向代理到容器)
22
+ └─ /ht32-bundles/* → bundle zip 静态目录(P1-3)
23
+ ```
24
+
25
+ 代码默认走 `stdio`(`MCP_TRANSPORT` 未设置时),supergateway 负责 stdio→Streamable HTTP 转换。
26
+ 与 ht32-wiki 同构:SSE 无心跳会被 OpenResty 空闲切断,故走 streamableHttp stateful。
27
+
28
+ ## 传输模式(0.2.2+ 支持原生 HTTP)
29
+
30
+ `server.py` 按 `MCP_TRANSPORT` 分派三种传输,**与 ht32-wiki 同构**:
31
+
32
+ | `MCP_TRANSPORT` | 行为 | 典型用途 |
33
+ |---|---|---|
34
+ | `stdio`(默认) | 标准输入输出 | 本地开发;或供 supergateway 包装为 Streamable HTTP |
35
+ | `streamable-http` | 单端点 HTTP,**默认有状态会话**(FastMCP `stateless_http=False`) | 直接对外暴露,无需 supergateway 这一层 |
36
+ | `sse` | 旧式 SSE(无心跳,易被反代空闲切断) | 兼容旧客户端,不推荐 |
37
+
38
+ HTTP 模式监听地址由 `MCP_HOST`(默认 `0.0.0.0`)/ `MCP_PORT`(默认 `8101`,
39
+ **避开 wiki 的 8100**)控制;端点路径为 FastMCP 默认的 `/mcp`。
40
+
41
+ ```bash
42
+ # 原生 Streamable HTTP(stateful),供反代直连
43
+ MCP_TRANSPORT=streamable-http MCP_HOST=0.0.0.0 MCP_PORT=8101 uvx ht32-code-mcp
44
+ ```
45
+
46
+ > **stateful 是默认值而非开关**:FastMCP 4.0.5 的 `stateless_http` 默认 `False`,
47
+ > 即同一 `mcp-session-id` 复用同一会话,子进程/进程内状态不每次 POST 重建——
48
+ > 与 supergateway 的 `--stateful --sessionTimeout 1800000` 语义一致。
49
+ > 若将来需要 stateless,须显式改 FastMCP 设置,届时 Wiki/Code 两端要一起对照。
50
+
51
+ ### 生产接线:一律用 supergateway 包装(2026-09-23 定)
52
+
53
+ 两个 MCP(wiki 与 code)**统一由 supergateway 包装**,不接受「服务自身原生 HTTP」形态:
54
+
55
+ - 容器里 `uvx ht32-code-mcp` 保持 stdio,由网关转 Streamable HTTP + stateful
56
+ - 好处:**网关这一层统一管协议版本与会话**(`--protocolVersion 2025-06-18`
57
+ `--stateful --sessionTimeout 1800000`),云端反代只认固定的容器端口/路径,
58
+ 换服务或改内部实现时反代无需跟着动
59
+
60
+ 原生 `streamable-http` / `sse` 保留为**能力**(本地直跑、调试、将来去网关时可用),
61
+ 但**当前生产不启用**;本地 compose 部署方案见本仓库 `.work/docker/docker-compose.yml`(按此约定编写)。
62
+
63
+ 无论走哪种,**监听端口必须绑 `0.0.0.0`**(默认值),否则网关/反代从容器外连不上。
64
+ 验证:`ss -ltnp | grep -E '1808[0-2]'` 应显示 `0.0.0.0`,而非 `127.0.0.1`。
65
+
66
+ ## 关键配置
67
+
68
+ 1Panel MCP 表单:
69
+
70
+ - 类型:`uvx`
71
+ - 启动命令:`uvx ht32-code-mcp`(**必须带 uvx 前缀**,容器里没装裸命令)
72
+ - 输出类型:`streamableHttp`
73
+ - 流式传输路径:`/ht32-code`(切输出类型后是**新字段**,默认空,必须手填;必填校验)
74
+ - 协议版本:`2025-06-18`(1Panel 预填默认值,勿动;仅用于 supergateway 对下游 stdio 的 auto-init)
75
+ - 镜像:`supercorp/supergateway:next-uvx`(**必须覆盖默认的 `:uvx`**——见下方踩坑)
76
+ - 参数:`--stateful --sessionTimeout 1800000`(30 分钟;2026-09-23 由 3600000 下调)
77
+ - 环境变量:`CODE_ROOT=/app/code`
78
+ - 挂载:`/opt/ht32-code:/app/code`(**不用加 `:ro`**——`index/code_index.json`
79
+ 已 gitignore 且容器内可能需重建,只读会阻断;容器路径不能有前导空格,否则 `invalid mount path`)
80
+
81
+ ### 镜像 tag 踩坑(2026-09-20)
82
+
83
+ 默认镜像 `supercorp/supergateway:uvx` 构建时解析到旧版
84
+ `@modelcontextprotocol/sdk`(LATEST_PROTOCOL_VERSION=2025-06-18),新版客户端
85
+ (Trae 发 `2025-11-25`)initialize 直接被拒:`Bad Request: Unsupported protocol
86
+ version`。根因是 supergateway 3.4.3 对 SDK 声明的是浮动依赖 `^1.18.2`,镜像构建
87
+ 日期决定行为;`next-uvx`(4.0.0-rc.4)将 SDK **精确锁定 1.30.0**,无此问题。
88
+
89
+ ## 云端 zip 下载通道(P1-3,0.1.2+)
90
+
91
+ `export_project_bundle` 有两种交付形态,**由服务端环境变量决定,调用方无需判断**:
92
+
93
+ | 模式 | 触发条件 | 行为 |
94
+ |---|---|---|
95
+ | 云端 zip | `BUNDLE_DIR` 与 `BUNDLE_BASE_URL` **同时非空** | 打包到 `BUNDLE_DIR`,返回 `<BUNDLE_BASE_URL>/<token>.zip` + SHA256 + 过期时间 |
96
+ | 本地目录 | 二者任一为空(默认) | 物化到 `out_dir`(须为空目录/不存在) |
97
+
98
+ ### 环境变量
99
+
100
+ | 变量 | 默认 | 说明 |
101
+ |------|------|------|
102
+ | `BUNDLE_DIR` | 空(= 关闭云端模式) | 容器内 bundle 输出目录,须可写 |
103
+ | `BUNDLE_BASE_URL` | 空(= 关闭云端模式) | 公网下载前缀,如 `https://mcp.s621.work/ht32-bundles` |
104
+ | `BUNDLE_TTL_SECONDS` | `3600` | zip 有效期;每次 export 前顺带清理过期 zip |
105
+
106
+ 非数字的 `BUNDLE_TTL_SECONDS` 会静默回退默认值,不报错。
107
+
108
+ ### 实现要点
109
+
110
+ - **token**:`secrets.token_urlsafe(24)`(192 bit 熵),文件名即 token;不可枚举、不可猜测
111
+ - **物化在系统临时目录**(`tempfile.mkdtemp`)而非 bundle 目录——bundle 目录里只出现
112
+ 最终 zip,中途失败的半成品不会暴露成可下载文件
113
+ - **清理**:`sweep_expired()` 在每次 export 时按 mtime 删除超期 `*.zip`;
114
+ 进程崩溃遗留的临时目录不影响交付(不在 bundle 目录内)
115
+ - **TTL 到点不主动删**:靠下次 export 的 sweep 或 P1-2 定时脚本清理;过期后
116
+ 重新调用工具即可拿新 URL
117
+
118
+ ### 服务器侧落地(3 步)
119
+
120
+ 本部署的 OpenResty 是**容器化**的(容器名 `1Panel-openresty-ih7J`),
121
+ 已挂载 `WEBSITE_DIR=/opt/1panel/www` → 容器内 `/www`。
122
+ 因此 bundle 目录直接落在网站根目录树下,**OpenResty 的 volumes 一行都不用改**——
123
+ 两个容器通过宿主机上的同一目录交换文件:
124
+
125
+ ```
126
+ 宿主机 /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles/ (站点 root 目录下 + /ht32-bundles)
127
+ ├─ code-mcp 容器看到 /app/bundles
128
+ └─ OpenResty 容器看到 /www/sites/mcp.s621.work/index/ht32-bundles
129
+ 公网 URL https://mcp.s621.work/ht32-bundles/<token>.zip
130
+ ```
131
+
132
+ 站点「网站目录」显示 root = `/opt/1panel/www/sites/mcp.s621.work/index`,
133
+ bundle 放其下子目录即被当作静态文件直接访问。
134
+
135
+ **第 1 步:宿主机建 bundle 目录**
136
+
137
+ ```bash
138
+ mkdir -p /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles
139
+ chmod 755 /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles
140
+ ```
141
+
142
+ **第 2 步:1Panel 给 code-mcp 容器补挂载与环境变量**
143
+
144
+ | 配置项 | 值 |
145
+ |--------|-----|
146
+ | 挂载 | `/opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles:/app/bundles`(**不要加 `:ro`,需可写**) |
147
+ | 环境变量 | `BUNDLE_DIR=/app/bundles` |
148
+ | 环境变量 | `BUNDLE_BASE_URL=https://mcp.s621.work/ht32-bundles` |
149
+ | 环境变量 | `BUNDLE_TTL_SECONDS=3600`(可选,默认即 3600) |
150
+
151
+ 完整配置(含现有项):
152
+
153
+ ```
154
+ 类型: uvx
155
+ 启动命令: uvx ht32-code-mcp
156
+ 输出类型: streamableHttp
157
+ 流式传输路径:/ht32-code
158
+ 协议版本: 2025-06-18
159
+ 镜像: supercorp/supergateway:next-uvx
160
+ 参数: --stateful --sessionTimeout 1800000
161
+ 环境变量: CODE_ROOT=/app/code
162
+ BUNDLE_DIR=/app/bundles
163
+ BUNDLE_BASE_URL=https://mcp.s621.work/ht32-bundles
164
+ 挂载: /opt/ht32-code:/app/code
165
+ /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles:/app/bundles ← 宿主机路径,非容器内路径
166
+ ```
167
+
168
+ **第 3 步:OpenResty 无需任何配置**
169
+
170
+ 站点 root = `/opt/1panel/www/sites/mcp.s621.work/index`,映射到容器内
171
+ `/www/sites/mcp.s621.work/index`。bundle 目录落在这个 root 之下子目录,
172
+ 静态文件天然可访问,**不用建反向代理、不用手写 location、不用改 compose**。
173
+
174
+ > **不要为 bundles 建反向代理条目**:1Panel 反代 UI 表达不了静态 location
175
+ > (后端代理地址必填,而静态目录没有后端)。若建了且后端留空,1Panel 生成的
176
+ > location 会抢走 `/ht32-bundles/` 却不知从哪读文件 → 404。已有此类条目请删除。
177
+
178
+ > **静态 location 无需 `proxy_*` 指令**:文件由 nginx 读盘直接返回,不用
179
+ > `proxy_pass`,也不用 `proxy_read_timeout`——这与 code 的代理 location 形态不同属正常。
180
+
181
+ > **权限**:1Panel 站点要求 `index` 目录下所有文件/目录属主与属组为 `1000`(见
182
+ > 「网站目录」页提示,PHP 容器环境)。nginx worker 需可读该目录;zip 文件 644 /
183
+ > 目录 755 正常可读。若验证时拿 403,先查属主属组与 SELinux 上下文。
184
+
185
+ 重载(改了容器挂载或环境变量时需重建 code-mcp;仅改文件无需重载 nginx):
186
+
187
+ ```bash
188
+ docker exec 1Panel-openresty-ih7J nginx -t
189
+ ```
190
+
191
+ ### 验证生效
192
+
193
+ 1. 调 `get_code_version`,应出现:
194
+
195
+ ```
196
+ 云端 zip 下载:启用(TTL 3600s,https://mcp.s621.work/ht32-bundles)
197
+ ```
198
+
199
+ 仍显示「未配置」→ 环境变量没注入成功(查变量名 / 容器是否重建)。
200
+
201
+ 2. 调 `export_project_bundle(chip="HT32F52352")`,应返回:
202
+
203
+ ```
204
+ 下载 URL:https://mcp.s621.work/ht32-bundles/<token>.zip
205
+ 有效期:3600 秒(至 ...)
206
+ 大小:1.53 MB(455 文件)
207
+ SHA256:...
208
+ ```
209
+
210
+ 3. 浏览器/curl 拉该 URL:应得约 1.5 MB zip;`sha256sum` 与报告一致;
211
+ 解压后 455 文件、`project/MDK_ARMv5/ht32f5-template.uvprojx` 在,UV4 编译 0E0W。
212
+
213
+ 4. 确认 `ls /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles` 只有 `*.zip`(无残留临时目录)。
214
+
215
+ ## 反馈与用量日志(P1-5,0.2.0+)
216
+
217
+ > **与 ht32-wiki-mcp 统一**:两服务使用同一套实现(`usage.py`,两仓库逐字节
218
+ > 相同的副本)、同一套目录约定(同根 + 服务子目录)、同一套字段。
219
+ > 本服务写 `<FEEDBACK_ROOT>/code/`,wiki 写 `<FEEDBACK_ROOT>/wiki/`。
220
+
221
+ 两条通道,按 UTC 日期分文件(JSON Lines):
222
+
223
+ | 通道 | 文件 | 触发 | 记录内容 |
224
+ |------|------|------|----------|
225
+ | 用量日志 | `usage_*.jsonl` | **服务端自动**(middleware 拦截所有工具调用) | 工具名、参数原文(剔除 `limit`/`max_length`/`out_dir`)、耗时、成败、错误、客户端 session |
226
+ | 反馈 | `feedback_*.jsonl` | **Agent 主动**(`submit_feedback` 工具) | 语义性问题:API 错 / 缺口 / 路径错 / 模板缺陷 |
227
+
228
+ 自动通道保证调用事实完整(不依赖 Agent 配合),用于追踪用户需求与生成的模板;
229
+ 反馈通道补 middleware 判断不了的语义问题。每条记录都带 `service` 字段
230
+ (`code`),便于两服务日志合并分析。
231
+
232
+ ### `ok` 字段的口径(0.2.1+)
233
+
234
+ 工具用**字符串**返回错误而不抛异常,middleware 拿不到协议级 `is_error`,
235
+ 故靠返回值文本判断。口径是**工具是否完成了它的工作**:
236
+
237
+ | 返回值 | `ok` | 说明 |
238
+ |---|---|---|
239
+ | `错误:…` / `导出失败…` | `false` | 参数校验、路径越界、IO 失败、无法产出工程 |
240
+ | `未找到…` / `不在支持列表` / `不在覆盖范围` / `暂无模板` | `true` | **"无数据"≠失败** |
241
+
242
+ "无数据"刻意记为成功:`用户搜 BLDC 但知识库没有` 正是指导 P2-1 补内容的
243
+ 最强信号,若标成失败会被分析工具当作噪声剔除。标记表在 `usage.py` 的
244
+ `SOFT_FAILURE_MARKERS`,**改动工具返回值文案时需同步维护**。
245
+
246
+ ### 目录布局
247
+
248
+ ```
249
+ /opt/ht32-mcp-feedback/ ← 宿主机单一入口,两个容器挂同一卷
250
+ ├─ wiki/ ← ht32-wiki 容器写这里
251
+ └─ code/ ← ht32-code 容器写这里(本服务)
252
+ ├─ usage_2026-09-21.jsonl
253
+ └─ feedback_2026-09-21.jsonl
254
+ ```
255
+
256
+ ### 环境变量
257
+
258
+ | 变量 | 默认 | 说明 |
259
+ |------|------|------|
260
+ | `FEEDBACK_ROOT` | 空(= **关闭**,本地开发默认不落盘) | 日志根目录,服务名子目录自动拼接;**必须填绝对路径**(如 `/app/feedback`,不要写 `app/feedback`) |
261
+ | `CODE_LOG_DIR` | — | 已废弃,改用 `FEEDBACK_ROOT` |
262
+
263
+ > ⚠️ **必须用绝对路径**:`FEEDBACK_ROOT=app/feedback` 会相对容器 WORKDIR
264
+ > 解析。实测云端曾因漏写前导 `/`,`get_code_version` 显示为
265
+ > `反馈与用量日志:启用(app/feedback/code)`——靠 WORKDIR 恰为 `/` 才侥幸
266
+ > 落对位置,改镜像 WORKDIR 后会静默写到别处。验证时看输出路径是否以 `/` 开头。
267
+
268
+ > **隐私**:参数按原文记录,`query` / `question` 可能含用户项目信息。
269
+ > 该目录只应挂在受控服务器上,**不要提交进仓库**(定时脚本是
270
+ > `git reset --hard` 镜像模式,写进仓库也会被覆盖丢失)。
271
+
272
+ ### 服务器侧落地
273
+
274
+ **第 1 步:宿主机建目录**
275
+
276
+ ```bash
277
+ mkdir -p /opt/ht32-mcp-feedback
278
+ chmod 755 /opt/ht32-mcp-feedback
279
+ ```
280
+
281
+ **第 2 步:两个 MCP 容器都挂同一卷**
282
+
283
+ | 服务 | 挂载 | 环境变量 |
284
+ |------|------|----------|
285
+ | ht32-wiki-mcp | `/opt/ht32-mcp-feedback:/app/feedback` | `FEEDBACK_ROOT=/app/feedback` |
286
+ | ht32-code-mcp | `/opt/ht32-mcp-feedback:/app/feedback` | `FEEDBACK_ROOT=/app/feedback` |
287
+
288
+ 完整配置(code 侧,含现有项):
289
+
290
+ ```
291
+ 类型: uvx
292
+ 启动命令: uvx ht32-code-mcp
293
+ 输出类型: streamableHttp
294
+ 流式传输路径:/ht32-code
295
+ 协议版本: 2025-06-18
296
+ 镜像: supercorp/supergateway:next-uvx
297
+ 参数: --stateful --sessionTimeout 1800000
298
+ 环境变量: CODE_ROOT=/app/code
299
+ BUNDLE_DIR=/app/bundles
300
+ BUNDLE_BASE_URL=https://mcp.s621.work/ht32-bundles
301
+ FEEDBACK_ROOT=/app/feedback
302
+ 挂载: /opt/ht32-code:/app/code
303
+ /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles:/app/bundles
304
+ /opt/ht32-mcp-feedback:/app/feedback
305
+ ```
306
+
307
+ ### 验证
308
+
309
+ 1. 调 `get_code_version`,应出现
310
+ `反馈与用量日志:启用(/app/feedback/code)`。
311
+ 仍显示「未配置」→ 环境变量没注入(查变量名 / 容器是否重建)。
312
+
313
+ 2. 随便调几个工具,然后看日志:
314
+
315
+ ```bash
316
+ tail -3 /opt/ht32-mcp-feedback/code/usage_$(date -u +%Y-%m-%d).jsonl
317
+ ```
318
+
319
+ 3. 统计被请求的芯片型号(指导 P2-1 排优先级)——用 ht32-skills 的分析工具:
320
+
321
+ ```bash
322
+ # 克隆 ht32-skills 后(或用本地副本)
323
+ python ht32-skills/scripts/analyze_feedback.py /opt/ht32-mcp-feedback
324
+ ```
325
+
326
+ 该工具(零依赖,仅标准库)合并 wiki + code 两服务日志,输出需求信号、
327
+ 调用分布与失败、反馈类型、耗时 p50/p95、最近失败与反馈明细;支持
328
+ `--days N` / `--service` / `--json`。详见 ht32-skills 的 README。
329
+
330
+ 4. 清理策略由定时同步脚本第 7 步负责(`LOG_KEEP_DAYS=30`,与同步日志同口径,
331
+ 目录变量为 `FEEDBACK_ROOT`)。
332
+
333
+ ## PyPI 包发布流程
334
+
335
+ ```bash
336
+ # token 在 ht32-code 根目录 .env 的 UV_PUBLISH_TOKEN
337
+ uv lock && uv build && uv publish
338
+ ```
339
+
340
+ 发布后同步 `VERSION` 文件末行:`MCP Server: <版本>(deployed, PyPI, <日期>)`。
341
+
342
+ ## 踩过的坑
343
+
344
+ ### 1. 云端跑的是 PyPI 包,不是 /opt 仓库(P0-1)
345
+
346
+ - **症状**:改了仓库代码、云端 MCP 行为不变
347
+ - **根因**:`uvx ht32-code-mcp` 从 PyPI 拉包,`/opt/ht32-code` 只是被检索的数据源
348
+ - **修复**:改代码必须**发版 + 重建容器**,改仓库文件本身不生效
349
+
350
+ ### 2. 索引只读挂载
351
+
352
+ - **症状**:容器启动报索引无法写入/重建失败
353
+ - **根因**:`/opt/ht32-code:/app/code:ro` 时 `index/code_index.json` 不可写
354
+ - **修复**:挂载去掉 `:ro`
355
+
356
+ ### 3. 镜像 tag 导致协议版本被拒
357
+
358
+ 见上方「镜像 tag 踩坑」。
359
+
360
+ ## 当前部署状态
361
+
362
+ | # | 任务 | 说明 | 状态 |
363
+ |---|------|------|------|
364
+ | 3.1 | MCP 工具实现(7 检索/导出 + 1 反馈) | `.work/mcp-server/src/ht32_code_mcp/`(FastMCP) | ✅ |
365
+ | 3.2 | 工具级测试 | `pytest` 46 用例全绿 | ✅ |
366
+ | 3.3 | PyPI 发布(hatchling) | `ht32-code-mcp` 0.2.2(2026-09-23,含 bundle 通道 + 统一反馈/用量日志 + 软失败识别 + 原生 Streamable HTTP) | ✅ |
367
+ | 3.4 | 云端 git clone + 挂载 | `/opt/ht32-code:/app/code` | ✅ |
368
+ | 3.5 | 1Panel 部署(uvx + supergateway → streamableHttp stateful,镜像 next-uvx) | `https://mcp.s621.work/ht32-code` | ✅ |
369
+ | 3.6 | OpenResty 超时修复(`proxy_read_timeout 3600s`) | 修复 60s 空闲断连;2026-09-21 复核两端空闲 100s 后调用仍 200,未被 1Panel 回退 | ✅ |
370
+ | 3.7 | 云端 zip 下载通道(P1-3) | 服务器侧已上线:下载 1608157 字节 / SHA256 一致 / 解压 455 文件 / UV4 0E0W | ✅ |
371
+ | 3.8 | 定时同步脚本含 bundle 清理(P1-2) | `ht32-code-mcp-update.md` 第 6 步 `find … -mmin +60 -delete` | ✅ |
372
+ | 3.9 | 反馈与用量日志(P1-5,与 wiki 统一) | 0.2.1;云端已启用,但 `FEEDBACK_ROOT` 漏写前导 `/`(显示 `app/feedback/code`),需改绝对路径 | ⚠️ |