被测对象:
service/cmd/mcp(MCP Server,名称fwh-tools,version2.0.0) 传输方式:stdio JSON-RPC(server.ServeStdio) 底层框架:github.com/mark3labs/mcp-go编写时间:2026-08-25
fwh-mcp-server 是一个通过 stdin/stdout 走 JSON-RPC 的 MCP Server。
入口 cmd/mcp/main.go 在启动时会初始化与 HTTP Server 相同的基础设施:
database.Init → MySQL / MongoDB / PgSQLcache.Init → RedisminioUtil.InitClient → MinIOrepositories.InitPlaneClient → Plane Web API随后调用 tools.RegisterAll(s) 注册全部工具,分为四层:
| 层级 | 前缀 | 说明 | 读写 |
|---|---|---|---|
| 业务逻辑层 | service_* |
调用后端 Service 层,含业务校验/聚合 | 读为主,含写 |
| 数据直查层 | data_* |
直接读 DB/Cache/远程 API | 只读为主 |
| 对象存储层 | storage_* |
MinIO / 本地 FileStore | 读写 |
| 辅助工具层 | util_* |
纯函数 | 只读 |
注意:
data_plane_v2_api_write.go中的写工具全部处于注释状态(未注册);registerPlaneApiTools(Plane V1)在data/register.go中已被停用,不会注册。 本方案以「实际注册的工具」为测试范围。
测试运行路径固定为:/var/www/fwh-toolbox/server
fwh-mcp-server 必须在该目录下作为当前工作目录(cwd)启动,原因见下方配置加载逻辑。
该目录实际内容(编写方案时探测):
| 文件 | 说明 |
|---|---|
fwh-mcp-server |
已构建的被测二进制(约 50MB,构建于 8月21日) |
fwh-toolbox-server |
HTTP Server 二进制(非本次测试对象) |
config.env |
配置文件(本部署使用的是 config.env,不是 .env) |
cert/ |
FWH MQTT / TLS 证书目录(相对路径引用) |
.kiro/ |
Agent / MCP 设置 |
config.LoadConfig)config.go 中按相对当前工作目录依次尝试:
paths := []string{"./.env", "./config.env"} // 先 .env,后 config.env
// 命中即停;随后 v.AutomaticEnv() 允许环境变量覆盖文件值
关键结论:
.env,只有 config.env → 会加载 ./config.env。/var/www/fwh-toolbox/server,否则读不到配置、会 fallback 到默认值(Mongo/Redis 走 localhost 默认、MySQL 为空、FWH 凭据缺失)。AutomaticEnv)——如需临时改依赖地址,可通过 env 注入。LoadConfig 在成功/失败时会向 stdout 打印:
✅ Using config file: ./config.env
⚠️ No config file found, using default values & environment variables
而 MCP 走 stdio JSON-RPC,这几行会污染通道。测试驱动(mcp-go client)在初始化握手时必须容忍/跳过这几行非 JSON 输出,或确认当前 mcp-go 版本对启动前的 stdout 噪声是否敏感。若握手失败,优先排查此处。
# 全部在测试运行路径下操作
cd /var/www/fwh-toolbox/server
# 复用已存在的二进制,或重新构建覆盖:
# (构建需在源码目录,产物移动到此路径)
# cd <源码>/service && go build -o fwh-mcp-server ./cmd/mcp/ && mv fwh-mcp-server /var/www/fwh-toolbox/server/
# 测试驱动以本目录为 cwd 启动 server(command="./fwh-mcp-server"),
# 与 .kiro/settings/mcp.json 中 "command": "./fwh-mcp-server" 一致。
测试驱动程序读取配置也应以
/var/www/fwh-toolbox/server/config.env为准, 保证「驱动断言用的依赖地址」与「server 实际连接的依赖地址」完全一致。
编写方案时对本机 127.0.0.1 的探测结果:
| 依赖 | 端口 | 状态 |
|---|---|---|
| Redis | 6379 | OPEN |
| MongoDB | 27017 | OPEN |
| MinIO | 9198 | OPEN |
| PgSQL | 5432 | OPEN |
| Plane Web | 8600 | OPEN |
| Go 工具链 | - | go1.26.4 可用 |
二进制 fwh-mcp-server |
- | 已存在于运行路径(构建于 8月21日),可复用或重建 |
配置来自 /var/www/fwh-toolbox/server/config.env(已核对实际内容):
MYSQL_DSN → 远程 34.211.183.235:3308/home_energy_storage_server(站点/SIM 等业务数据依赖)MONGO_DSN → 127.0.0.1:27017/FwhToolboxDb(注意:生产库名 FwhToolboxDb,非 dev 库)POSTGRES_DSN → localhost:5432/fwh_toolbox_dbREDIS_ADDR → localhost:6379MINIO_ENDPOINT → localhost:9198,bucket fwh-toolboxenergy.franklinwh.com,账号 FMU006(含真实密码,凭据可用性需实测)http://localhost:8600,workspace fwh(含真实 Web/API 凭据)结论:Mongo/Redis/MinIO/PgSQL/Plane 均可连通且有真实凭据;FWH 远程 API 与远程 MySQL 有真实凭据,但可用性/网络连通性仍需在测试时实际验证。
⚠️ 数据敏感性:Mongo 指向生产库
FwhToolboxDb,MySQL 为生产业务库。因此data_mongo_*、业务service_*查询会读到真实数据,L3 写操作尤其需谨慎——默认一律不真实写入。
按依赖强度从低到高分为 L0–L3,逐层推进。低层是高层的前置门禁。
目的:验证 server 能正常握手、所有工具正确注册、schema 合法。
| 用例 | 方法 | 断言 |
|---|---|---|
| L0-1 初始化握手 | initialize |
返回 serverInfo.name == "fwh-tools"、version == "2.0.0"、protocolVersion 合法 |
| L0-2 工具清单 | tools/list |
返回工具数 > 0,且能全部列出(处理分页 nextCursor) |
| L0-3 工具命名规范 | 解析清单 | 每个工具名前缀属于 service_/data_/storage_/util_/plane_ 集合 |
| L0-4 Schema 完整性 | 解析清单 | 每个工具含 name/description/inputSchema,required 字段声明合法 |
| L0-5 未知工具 | tools/call name=不存在 |
返回错误(非崩溃) |
L0 全部不依赖任何后端数据,是回归测试的核心。
目的:在不依赖外部服务的前提下验证工具逻辑与错误处理。
| 用例 | 工具 | 输入 | 断言 |
|---|---|---|---|
| L1-1 SN 解析正确性 | util_parse_sn |
sn=10060006A03X25310091 |
productName 含 aGate、year=2025、week=31、serialNumber=0091 |
| L1-2 SN 缺参 | util_parse_sn |
无 sn |
isError,提示 参数 sn 必填 |
| L1-3 SN 长度不足 | util_parse_sn |
sn=1006 |
isError,提示长度不足 |
| L1-4 必填缺失(抽样) | service_site_query_info 等 |
省略 required | isError,参数 xxx 必填 |
| L1-5 格式校验 | service_site_power_flow |
date=2025/01/01(错误格式) |
isError,提示需 yyyy-MM-dd |
说明:L1-4/L1-5 只验证「参数校验分支」,在 handler 调用外部服务之前返回,因此不依赖后端。
目的:对依赖可用的只读工具做真实调用,验证「能连通 + 返回结构合法 + 无 panic」。
断言以结构合法性为主(返回是 text content、可 JSON 解析或为结构化错误),不强断言具体业务数据。
分组(按依赖):
data_redis_get、data_redis_existsdata_mongo_find、data_mongo_count(注意 collection 白名单)data_pgsql_query_snapshot_file_record、data_pgsql_query_snapshot_datastorage_minio_list_objects、storage_minio_read_object、storage_file_read、storage_file_listplane_list_workspaces、plane_list_projects、plane_list_issues、plane_list_labels、plane_list_states、service_feedback_history 等service_site_search、service_sim_search、service_monitor_alarm_stats、service_dashboard_*、service_tou_template_list、service_background_task_list、service_site_monitor_config_list 等data_fwh_*、service_site_query_info、service_site_power_flow、data_sim_*每个用例三态判定:
凭据类失败(如 FWH 密码占位、Plane 登录失败)归为 SKIP 并标注,不算 FAIL。
写操作工具(描述带 [写操作])默认只测参数校验分支,不触发真实写入,避免污染数据。
仅在你显式确认使用隔离/测试环境后,才追加"真实写入 + 回滚/清理"用例。
已注册的写操作工具(全部为 service_*):
| 工具 | 动作 |
|---|---|
service_feedback_create |
在 Plane 创建 Intake Issue |
service_site_info_trigger_update |
触发站点信息同步任务 |
service_site_info_tag_update |
创建/更新站点标签 |
service_auto_update_create_package / _update_package / _delete_package |
升级包增改删 |
service_auto_update_update_config / _delete_config |
升级配置改删 |
service_auto_update_create_file |
创建升级文件 |
service_batch_download_create / _update_desc / _update_owner / _delete |
批量下载任务增改删 |
service_site_analysis_create / _update_desc / _delete |
站点分析任务增改删 |
service_site_monitor_config_create / _update / _delete |
站点监控配置增改删 |
storage 写:storage_file_write、storage_file_upload、storage_file_read_to_local —— 默认只测参数校验。
背景:MCP 输出会经过
helper层的 PII 脱敏后才返回给 Agent(JSONResult/JSONResultOrFile/BytesResultOrFileMasked)。 由于 Mongo/MySQL 指向生产库,含真实用户 PII,脱敏是否生效直接关系到数据是否泄露给上层 LLM/日志。这一层必须验证。
cmd/mcp/tools/helper/)双路径 + fail-closed:
*dto.SiteInfoDTO、domain.SimCardInfo、dto.SimCardInfo、ResPage[...] 等已知类型时,对副本做字段级脱敏后序列化(不污染上游共享对象)。脱敏规则(pii_rules.go):
| 类别 | 字段(小写匹配) | 方式 | 结果 |
|---|---|---|---|
| 手机号 | phone/mobile/tel/contactnumber/msisdn/financiercontractnumber | MaskFull | 固定 6 位 ****** |
| 姓名 | username/firstname/lastname/accountname/financiername/display_name/displayname | MaskName | 保留首字符 + *** |
| 邮箱 | MaskEmail | 首字符 + *** + 完整域名 |
|
| 账号 | account/installeraccount | MaskAccount | 前2 + *** + 后2 |
| 地址串 | address/deviceaddress/addresssecondlast/gpsaddress | MaskFull | ****** |
| 经纬度 | longitude/latitude/lng/lat | MaskFull | ****** |
| 设备/网络标识 | imei/imsi/mac/deviceip/gatewayip | MaskFull | ****** |
| 保留(不脱敏) | iccid/giccid/sn、city/province/country | — | 原值 |
cmd/mcp/tools/helper/pii_test.go 已覆盖掩码函数、流式脱敏(扁平/嵌套/子树/非法JSON fail-closed/null保留)、强类型脱敏与上游不污染、A/B 一致性。
go test ./cmd/mcp/tools/helper/...,全部 PASS 是后续端到端脱敏验证的前提。通过 MCP 真实调用,验证「返回给 Agent 的内容确实已脱敏」。断言方式:对返回的 content 文本做敏感值检测——不得出现原始 PII,且应出现对应掩码形态。
| 用例 | 工具 | 断言(对返回文本) |
|---|---|---|
| L4-1 站点详情脱敏 | service_site_query_info(真实 SN) |
不含明文手机号/邮箱/完整地址/经纬度/IMEI/MAC/IP;contactNumber/address/longitude/latitude/imei 值为 ******;userName 形如 X***;email 形如 x***@域名;保留 sn、iccId、city 原值 |
| L4-2 站点列表脱敏 | service_site_search |
分页 Data[] 内每个站点均脱敏(验证 ResPage 强类型路径生效) |
| L4-3 SIM 详情脱敏 | service_sim_query(真实 GICCID) |
imei/imsi/msisdn 为 ******;保留 giccid |
| L4-4 SIM 列表脱敏 | service_sim_search |
Data[] 内每张卡的 imei/imsi/msisdn 均脱敏,giccid 保留 |
| L4-5 直查层脱敏(B 路径) | data_mongo_find(含 PII 的白名单 collection) |
走流式脱敏;返回中敏感 key 的 value 已脱敏(验证未命中强类型时 B 路径仍生效) |
| L4-6 FWH 原始响应脱敏 | data_fwh_*(返回上游原始 JSON 的工具) |
走 BytesResultOrFileMasked,敏感字段已脱敏 |
| L4-7 落盘结果脱敏 | 触发大结果落盘(spilled:true)的查询 |
读取 localPath 文件内容,文件内也是脱敏后的数据(脱敏发生在落盘之前,见 marshalSanitized → BytesResultOrFile) |
| L4-8 fail-closed 行为 | 见风险说明 | 脱敏失败时返回 error 而非原始数据(此项主要靠单测 L4-0 保证,端到端难构造,标注为设计验证) |
驱动程序应对返回文本执行如下"负向断言"(这些明文模式不得出现):
\b1\d{10}\b)[\w.+-]+@[\w.-]+(除已知保留域名场景外,本地部分应已被 x*** 替换)([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b\d{1,3}(\.\d{1,3}){3}\b(排除脱敏后的 ******)longitude/latitude 的 value 不应为数字并执行"正向断言":保留字段(sn/iccid/giccid/city)应保持原值。
注意:L4 端到端用例依赖能查到含 PII 的真实记录。需要你提供 1 个真实站点 SN 和 1 个真实 GICCID 作为测试样本;若不便,可退化为"L4-0 单测 + L4-5 直查一条已知含 PII 的 Mongo 文档"。
推荐 方式 A:独立 Go 测试驱动,最适合"所有接口都测一遍"且可重复、可 CI。
在 cmd/mcp/test/ 下编写一个 Go 程序 / go test,用 mcp-go 的 client 以 stdio 方式拉起编译好的 fwh-mcp-server,然后:
initialize → 断言 L0-1ListTools(翻页聚合)→ 断言 L0-2~L0-4,导出完整工具清单CallTool 执行 L1/L2/L3 用例go test ./cmd/mcp/tools/helper/... 作为 L4-0)优点:类型安全、断言强、可重复运行、结果可机读。 关键点:
/var/www/fwh-toolbox/server(否则读不到 config.env)。command="./fwh-mcp-server"、workingDir="/var/www/fwh-toolbox/server"。用 shell/python 向 server stdin 逐条喂报文:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"util_parse_sn","arguments":{"sn":"10060006A03X25310091"}}}
适合快速手工验证 L0/L1,断言能力弱,不适合全量回归。
@fwh-tools 实测最贴近真实使用(含 Agent 权限确认、落盘规则),但难以批量断言,适合最终验收抽查。
运行路径固定为 /var/www/fwh-toolbox/server(配置文件 config.env 所在目录)。
# 方案 1:复用运行路径下已有的二进制(推荐,最贴近部署态)
cd /var/www/fwh-toolbox/server
# 二进制 ./fwh-mcp-server 已存在
# 方案 2:如需重建,在源码目录构建后覆盖到运行路径
# cd <源码>/service
# go build -o fwh-mcp-server ./cmd/mcp/
# mv fwh-mcp-server /var/www/fwh-toolbox/server/
# 运行测试驱动(驱动程序位于源码 cmd/mcp/test/ 下)
# 驱动内部以 cwd=/var/www/fwh-toolbox/server 拉起 ./fwh-mcp-server
cd <源码>/service
go run ./cmd/mcp/test/runner # 或 go test ./cmd/mcp/test/...
# 查看生成的报告
cat /var/www/fwh-toolbox/server/report.md # 或驱动指定的输出路径
配置由 server 自身通过 config.LoadConfig() 从 cwd 的 config.env 读取;
测试驱动如需断言依赖地址,应读取同一份 /var/www/fwh-toolbox/server/config.env,
保证「驱动断言的依赖」与「server 实际连接的依赖」一致。无需手工导出环境变量。
go test ./cmd/mcp/tools/helper/... 全部 PASScmd/mcp/test/(方式 A 的 runner 或 _test.go)cmd/mcp/test/tools_snapshot.json(tools/list 导出)cmd/mcp/test/report.md,每个工具一行 PASS/FAIL/SKIP + 原因 + 耗时cmd/mcp/test/pii_report.md,逐条列出 L4 用例的负向/正向断言结果;对每个端到端用例附「脱敏后返回样本(已确保无明文 PII)」以便复核/var/log/fwh-mcp-server/mcp.log(该目录需写权限,否则 fallback 到 nop logger)。config.LoadConfig() 会向 stdout 打印 ✅ Using config file: ./config.env,这会混入 JSON-RPC 通道。驱动握手需容忍启动阶段的非 JSON 行;若握手失败优先排查此处。"spilled": true 时,完整内容在 localPath(/tmp/fwh-toolbox-agent/),preview 仅片段——断言时需识别此情况。data_mongo_* 受 collection 白名单、storage_minio_* 受 bucket 白名单约束,用例需使用白名单内对象。config.env 含真实 FWH(FMU006)、Plane 凭据;Mongo/MySQL 指向生产库。只读查询会读到真实数据,写操作默认一律不真实执行。FWH 远程 API 与远程 MySQL 的网络连通性仍需实测,失败归 SKIP。确认后我再实现测试驱动并执行。