# FWH MCP Server 接口测试方案 > 被测对象:`service/cmd/mcp`(MCP Server,名称 `fwh-tools`,version `2.0.0`) > 传输方式:stdio JSON-RPC(`server.ServeStdio`) > 底层框架:`github.com/mark3labs/mcp-go` > 编写时间:2026-08-25 --- ## 1. 被测对象概况 `fwh-mcp-server` 是一个通过 **stdin/stdout 走 JSON-RPC** 的 MCP Server。 入口 `cmd/mcp/main.go` 在启动时会初始化与 HTTP Server 相同的基础设施: - `database.Init` → MySQL / MongoDB / PgSQL - `cache.Init` → Redis - `minioUtil.InitClient` → MinIO - `repositories.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` 中**已被停用**,不会注册。 > 本方案以「实际注册的工具」为测试范围。 --- ## 2. 运行路径与配置文件(关键) **测试运行路径固定为:`/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` 中按**相对当前工作目录**依次尝试: ```go paths := []string{"./.env", "./config.env"} // 先 .env,后 config.env // 命中即停;随后 v.AutomaticEnv() 允许环境变量覆盖文件值 ``` 关键结论: 1. 该目录下**没有 `.env`,只有 `config.env`** → 会加载 `./config.env`。 2. 因为是相对路径,**server 与测试驱动都必须把 cwd 设为 `/var/www/fwh-toolbox/server`**,否则读不到配置、会 fallback 到默认值(Mongo/Redis 走 localhost 默认、MySQL 为空、FWH 凭据缺失)。 3. 环境变量可覆盖文件配置(`AutomaticEnv`)——如需临时改依赖地址,可通过 env 注入。 ### ⚠️ stdout 污染风险(必须处理) `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 噪声是否敏感。若握手失败,优先排查此处。 ### 运行约定(方式 A 落地到该路径) ```sh # 全部在测试运行路径下操作 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 实际连接的依赖地址」完全一致。 --- ## 3. 环境前置(本机探测结果) 编写方案时对本机 `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_db` - `REDIS_ADDR` → `localhost:6379` - `MINIO_ENDPOINT` → `localhost:9198`,bucket `fwh-toolbox` - FWH API → `energy.franklinwh.com`,账号 `FMU006`(**含真实密码**,凭据可用性需实测) - Plane → `http://localhost:8600`,workspace `fwh`(**含真实 Web/API 凭据**) > 结论:Mongo/Redis/MinIO/PgSQL/Plane 均可连通且有真实凭据;FWH 远程 API 与远程 MySQL 有真实凭据,但可用性/网络连通性仍需在测试时实际验证。 > > ⚠️ 数据敏感性:Mongo 指向**生产库 `FwhToolboxDb`**,MySQL 为**生产业务库**。因此 `data_mongo_*`、业务 `service_*` 查询会读到真实数据,L3 写操作尤其需谨慎——**默认一律不真实写入**。 --- ## 3. 测试分层策略 按依赖强度从低到高分为 L0–L3,逐层推进。低层是高层的前置门禁。 ### L0 —— 协议与工具清单(无外部数据依赖,必测) 目的:验证 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 —— 纯函数与参数校验(可精确断言) 目的:在不依赖外部服务的前提下验证工具逻辑与错误处理。 | 用例 | 工具 | 输入 | 断言 | |------|------|------|------| | 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 调用外部服务**之前**返回,因此不依赖后端。 ### L2 —— 只读工具连通性冒烟(依赖外部服务) 目的:对依赖可用的只读工具做真实调用,验证「能连通 + 返回结构合法 + 无 panic」。 断言以**结构合法性**为主(返回是 `text` content、可 JSON 解析或为结构化错误),不强断言具体业务数据。 分组(按依赖): - **Redis 依赖**:`data_redis_get`、`data_redis_exists` - **Mongo 依赖**:`data_mongo_find`、`data_mongo_count`(注意 collection 白名单) - **PgSQL 依赖**:`data_pgsql_query_snapshot_file_record`、`data_pgsql_query_snapshot_data` - **MinIO 依赖**:`storage_minio_list_objects`、`storage_minio_read_object`、`storage_file_read`、`storage_file_list` - **Plane 依赖**:`plane_list_workspaces`、`plane_list_projects`、`plane_list_issues`、`plane_list_labels`、`plane_list_states`、`service_feedback_history` 等 - **MySQL/业务依赖**:`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` 等 - **FWH 远程 API 依赖**:`data_fwh_*`、`service_site_query_info`、`service_site_power_flow`、`data_sim_*` 每个用例三态判定: - **PASS**:返回 content 且结构合法(成功数据 或 业务空结果) - **FAIL**:进程 panic / 返回非法结构 / 超时无响应 - **SKIP(dependency)**:依赖不可用导致的连通错误(记录原因,不计失败) > 凭据类失败(如 FWH 密码占位、Plane 登录失败)归为 SKIP 并标注,不算 FAIL。 ### L3 —— 写操作工具(默认只做参数校验,不真实写入) 写操作工具(描述带 `[写操作]`)默认**只测参数校验分支**,不触发真实写入,避免污染数据。 仅在你**显式确认使用隔离/测试环境**后,才追加"真实写入 + 回滚/清理"用例。 已注册的写操作工具(全部为 `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` —— 默认只测参数校验。 ### L4 —— 敏感数据脱敏验证(PII,重点,必测) > 背景:MCP 输出会经过 `helper` 层的 PII 脱敏后才返回给 Agent(`JSONResult` / `JSONResultOrFile` / `BytesResultOrFileMasked`)。 > 由于 Mongo/MySQL 指向**生产库**,含真实用户 PII,脱敏是否生效直接关系到数据是否泄露给上层 LLM/日志。**这一层必须验证。** #### 脱敏机制(`cmd/mcp/tools/helper/`) 双路径 + fail-closed: - **优化 A(强类型源头脱敏)**:命中 `*dto.SiteInfoDTO`、`domain.SimCardInfo`、`dto.SimCardInfo`、`ResPage[...]` 等已知类型时,对**副本**做字段级脱敏后序列化(不污染上游共享对象)。 - **优化 B(JSON 流式脱敏)**:未命中强类型时,序列化后按 token 流式扫描,命中敏感 key 则脱敏其 value;敏感 key 对应对象/数组时**整个子树完全脱敏**。 - **fail-closed**:脱敏解析失败时**拒绝输出**(返回 error),绝不泄露原始数据。 脱敏规则(`pii_rules.go`): | 类别 | 字段(小写匹配) | 方式 | 结果 | |------|------|------|------| | 手机号 | phone/mobile/tel/contactnumber/msisdn/financiercontractnumber | MaskFull | 固定 6 位 `******` | | 姓名 | username/firstname/lastname/accountname/financiername/display_name/displayname | MaskName | 保留首字符 + `***` | | 邮箱 | email | 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 一致性。 - **L4-0**:先运行 `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 保证,端到端难构造,标注为设计验证) | #### 敏感值检测清单(供 L4-1~L4-7 自动断言) 驱动程序应对返回文本执行如下"负向断言"(这些**明文模式不得出现**): - 手机号:连续 11 位数字(`\b1\d{10}\b`) - 邮箱明文:`[\w.+-]+@[\w.-]+`(除已知保留域名场景外,本地部分应已被 `x***` 替换) - IMEI/IMSI:连续 14–15 位数字 - MAC:`([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}` - IP:`\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 文档"。 --- ## 4. 执行方式 推荐 **方式 A:独立 Go 测试驱动**,最适合"所有接口都测一遍"且可重复、可 CI。 ### 方式 A:Go 测试驱动(推荐) 在 `cmd/mcp/test/` 下编写一个 Go 程序 / `go test`,用 `mcp-go` 的 **client** 以 stdio 方式拉起编译好的 `fwh-mcp-server`,然后: 1. `initialize` → 断言 L0-1 2. `ListTools`(翻页聚合)→ 断言 L0-2~L0-4,导出完整工具清单 3. 逐个 `CallTool` 执行 L1/L2/L3 用例 4. 执行 L4 敏感数据脱敏用例(含调用前先跑 `go test ./cmd/mcp/tools/helper/...` 作为 L4-0) 5. 汇总生成报告(PASS/FAIL/SKIP + 原因) 优点:类型安全、断言强、可重复运行、结果可机读。 关键点: - server 与驱动的 **cwd 必须为 `/var/www/fwh-toolbox/server`**(否则读不到 `config.env`)。 - 驱动启动 server 时 `command="./fwh-mcp-server"`、`workingDir="/var/www/fwh-toolbox/server"`。 - 给每个 call 设置超时(远程 API 建议 15~30s)。 - 握手时容忍 server 启动阶段可能出现的 stdout 噪声(见 §2 stdout 污染)。 ### 方式 B:裸 JSON-RPC 脚本(轻量补充) 用 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,断言能力弱,不适合全量回归。 ### 方式 C:通过 Kiro CLI 加载 `@fwh-tools` 实测 最贴近真实使用(含 Agent 权限确认、落盘规则),但难以批量断言,适合最终验收抽查。 --- ## 5. 执行步骤(方式 A) **运行路径固定为 `/var/www/fwh-toolbox/server`(配置文件 `config.env` 所在目录)。** ```sh # 方案 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 实际连接的依赖」一致。无需手工导出环境变量。 --- ## 6. 通过标准 - **L0 全部 PASS**(协议与注册无问题)——硬性门禁 - **L1 全部 PASS**(纯函数与参数校验正确)——硬性门禁 - **L2**:依赖可用的工具 PASS;依赖不可用/凭据失效的记为 SKIP 并在报告说明;**不允许出现 FAIL(panic/非法结构/超时)** - **L3**:参数校验用例全部 PASS;真实写入用例仅在确认测试环境后执行 - **L4 敏感数据脱敏(硬性门禁,安全相关)**: - L4-0 单元测试 `go test ./cmd/mcp/tools/helper/...` 全部 PASS - L4-1~L4-7 端到端用例中,**任何返回给 Agent 的内容出现明文 PII 即判定 FAIL(安全缺陷,最高优先级)** - 保留字段(sn/iccid/giccid/city 等)被误脱敏也判定 FAIL(可用性缺陷) - 落盘文件(L4-7)内容必须同样已脱敏 --- ## 7. 交付产物 1. 测试驱动代码:`cmd/mcp/test/`(方式 A 的 runner 或 `_test.go`) 2. 完整工具清单快照:`cmd/mcp/test/tools_snapshot.json`(`tools/list` 导出) 3. 测试报告:`cmd/mcp/test/report.md`,每个工具一行 PASS/FAIL/SKIP + 原因 + 耗时 4. **敏感数据脱敏专项报告**:`cmd/mcp/test/pii_report.md`,逐条列出 L4 用例的负向/正向断言结果;对每个端到端用例附「脱敏后返回样本(已确保无明文 PII)」以便复核 --- ## 8. 风险与注意事项 - **stdio 污染(两处)**: - server 运行日志已重定向到 `/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。 - **写操作**:默认不真实写入。若需真实写入验证,务必确认为隔离环境并准备数据清理。 - **超时**:远程 API 调用给足超时(15~30s),避免误判为 FAIL。 --- ## 9. 待你确认的点 1. **测试范围**:只做 L0+L1(协议+纯函数),还是 L0~L2 全量只读冒烟?L3 是否需要真实写入?(L4 敏感数据脱敏建议无论如何都做,属安全门禁) 2. **执行方式**:确认用方式 A(Go 驱动)? 3. **数据环境**:Mongo/MySQL 指向**生产库**,只读查询可接受吗?还是需要切到隔离库再测? 4. **写操作**:是否允许对 Plane / 本地 DB 做真实写入(会产生测试数据)?默认不写。 5. **L4 脱敏样本**:能否提供 1 个真实站点 SN + 1 个真实 GICCID(用于端到端脱敏验证)?若不便,退化为「单测 L4-0 + 直查一条已知含 PII 的 Mongo 文档」。 确认后我再实现测试驱动并执行。