# FWH Agent MCP:设计 > 状态:设计初稿,2026-10-24。基于 ToolBox master `a10cbda`、AgentServer `969a993`(ADR-0021 第三方 MCP 服务)。本文描述为新 AI 助手**新建**的 MCP Server(下称 Agent MCP),与旧助手的 `service/cmd/mcp` 无关:旧 MCP 保持现状继续服务 Kiro,新 MCP 不复用其工具代码。 ## 1. 定位 Agent MCP 是 FWH 业务数据的**无用户状态的工具服务**: - 只回答“数据是什么”,不知道、也不关心是谁在问。没有 `userId`、`sessionId` 参数,不按用户隔离数据,不记录调用人。 - 由 AgentServer 后台“MCP 服务”登记为远程 HTTP 服务(ADR-0021),经 MCP Gateway 转接给 Agent。Gateway 用固定请求头连接、多个会话共用连接,本来就不传会话信息,与“无用户”的定位一致。 - 谁能用、能用到什么程度,由 ToolBox 和 AgentServer 在上层控制:ToolBox 决定用户能否使用 AI 助手、创建会话时选用哪个 Profile;Profile 决定勾选哪些 MCP 服务;AgentServer 按工具策略决定是否审批。调用追溯走“ToolBox 会话归属 → AgentServer 审计(gateway_call 含参数)”,见 [technical-design.md](technical-design.md)。 ### 1.1 为什么不沿用旧 MCP | 旧 MCP 的做法 | 在新框架下的问题 | | --- | --- | | 工具参数里带 `userId`/`sessionId`,由模型填写(`storage_*`、反馈、批量下载 owner 等) | 身份不可信;新框架不传用户信息,参数失去意义 | | 大结果写到 `/tmp/fwh-toolbox-agent/results` 并返回本地路径 | AgentServer 的 Agent 在沙箱里,看不到这个目录 | | 文件经 MinIO `upload/`、`download/` 前缀与 Agent 交换 | 新框架的上传和产出都在 AgentServer 会话工作区 | | 222 个工具都没有声明 `readOnlyHint` | ADR-0021 默认策略下全部变成“跟随权限模式”,每次查询都弹审批;逐个手工设置也难以维护 | | 大量一对一包装接口和原始存储访问(`data_mongo_*`、`data_redis_*`、`data_pgsql_*`、Plane 全量接口) | 工具多、描述短,模型选错工具;原始访问的结果大、难控制 | | 只有 stdio | 只能作为本地进程接入;本地进程在 AgentServer 的 bwrap 沙箱中运行,要开放网络和读取 `.env`,不合适 | | 依赖 Kiro steering 中的路由表决定用哪个工具 | 新框架没有 steering,工具描述要能自解释 | ## 2. 设计原则 1. **无用户**:不接收、不推断调用人;写操作(若有)以固定的服务账号身份执行。 2. **任务粒度**:一个工具对应一个排查/分析动作(“查站点概况”“查一段时间的告警事件”),在服务端完成过滤、分页、聚合,而不是暴露底层接口或存储。 3. **结果小而明确**:默认返回紧凑结果并给出总数与是否截断;需要大量数据时用专门的导出工具,以文件形式交给 Agent(见第 5 节),不把明细塞进上下文。 4. **自解释**:工具描述写清用途、适用场景、参数格式、单位和返回字段;不依赖外部路由表。 5. **全部显式声明读写**:每个工具声明 `readOnlyHint`,由测试保证。 6. **读写分离部署**:只读工具与写工具放在两个 MCP 服务中(第 7 节),Profile 按需勾选,权限边界清晰。 ## 3. 框架 ### 3.1 位置与目录 放在 ToolBox 仓库,与主服务同一个 Go module,直接复用 `repositories`、`services`、`dto`、`config`。 ``` service/ cmd/agent-mcp/main.go 入口:读取配置、初始化基础设施、启动 HTTP 服务 agentmcp/ bootstrap.go 初始化 DB / Redis / MinIO / repositories / FWH API / Plane(与主服务相同的依赖,只取需要的部分) server.go NewServer(set):创建 MCPServer、挂中间件、按工具集注册 http.go Streamable HTTP(无状态)+ Bearer 鉴权 + /healthz kit/ tool.go 工具定义 Def[T]、注册函数 result.go 结果构造:Object / List / Table / Export,统一的截断与大小上限 errors.go UserError:给模型看的错误信息(参数不对、无数据、上游不可用) middleware.go 日志、panic 恢复、单次超时、并发上限 tools/ site.go alarm.go shutdown.go logs.go faultrecord.go tou.go sim.go emspara.go plane.go util.go readonly.go 只读工具集 ops.go 写工具集(第 7 节,二期) agentmcp_test.go 约束测试(第 3.5 节) utils/pii/ 从 cmd/mcp/tools/helper 的 pii_*.go 拷贝为独立包(旧 MCP 不改) ``` 同一个二进制,用参数选择工具集: ``` fwh-agent-mcp -set=readonly -listen=127.0.0.1:8091 # 只读服务 fwh-agent-mcp -set=ops -listen=127.0.0.1:8092 # 写服务(二期) ``` ### 3.2 工具定义 使用 mcp-go v0.57(`go.mod` 当前版本)的结构体生成参数 schema(`mcp.WithInputSchema[T]`)和类型化处理函数(`mcp.NewTypedToolHandler`),参数校验交给 `server.WithInputSchemaValidation()`: ```go // kit/tool.go type Def[T any] struct { Name string // <领域>_<动作>,小写下划线,≤ 40 字符 Title string // 中文短标题,审批卡片与后台显示 Description string // 用途、适用场景、返回字段与单位 ReadOnly bool Handler func(ctx context.Context, args T) (*mcp.CallToolResult, error) } func (d Def[T]) Register(s *server.MCPServer) { s.AddTool( mcp.NewTool(d.Name, mcp.WithDescription(d.Description), mcp.WithInputSchema[T](), mcp.WithTitleAnnotation(d.Title), mcp.WithReadOnlyHintAnnotation(d.ReadOnly), mcp.WithDestructiveHintAnnotation(false), mcp.WithOpenWorldHintAnnotation(false), ), mcp.NewTypedToolHandler(func(ctx context.Context, _ mcp.CallToolRequest, args T) (*mcp.CallToolResult, error) { return d.Handler(ctx, args) }), ) } ``` 一个工具的写法: ```go // tools/alarm.go type alarmEventsArgs struct { SN string `json:"sn" jsonschema:"站点 SN(aGate 序列号)"` From string `json:"from" jsonschema:"开始时间,yyyy-MM-dd HH:mm:ss,站点当地时间"` To string `json:"to" jsonschema:"结束时间,同上;与开始时间相差不超过 31 天"` Kind string `json:"kind,omitempty" jsonschema:"alarm / event / all,默认 all"` Limit int `json:"limit,omitempty" jsonschema:"最多返回条数,1~500,默认 100"` } var AlarmEvents = kit.Def[alarmEventsArgs]{ Name: "alarm_events", Title: "查询站点告警/事件记录", Description: "查询单个站点在一段时间内的告警和事件记录,按时间倒序。用于排查某次故障前后发生了什么。" + "返回字段:time(站点当地时间)、code、name、level、device、status。超过 limit 时返回 truncated=true,请缩短时间范围。", ReadOnly: true, Handler: func(ctx context.Context, a alarmEventsArgs) (*mcp.CallToolResult, error) { from, to, err := kit.ParseRange(a.From, a.To, 31*24*time.Hour) if err != nil { return kit.Fail(err), nil } rows, err := repositories.QueryEventOrAlarmListOnTimeRange(a.SN, from, to, kindOf(a.Kind)) if err != nil { return kit.Upstream("FWH 平台", err), nil } return kit.List(mapAlarmRows(rows), kit.Limit(a.Limit, 100)), nil }, } ``` 处理函数只做三件事:校验与换算参数、调用 `repositories`/`services`、把上游数据映射成精简的行结构。字段映射集中写,丢弃对分析无用的字段,统一时间格式与单位。 schema 由 mcp-go 依赖的 `google/jsonschema-go` 生成:`jsonschema` 标签的内容就是字段描述(不支持 `key=value` 选项),没有 `omitempty` 的字段为必填。枚举、取值范围写进描述,并在处理函数中校验(`kit.Enum`、`kit.Limit` 等辅助函数);需要在 schema 中声明枚举时,在 `Def.Register` 里对生成的 schema 做补充。AgentServer 网关只接受 `type: object` 的 inputSchema,测试中要检查。 ### 3.3 结果构造 `kit/result.go` 提供统一的返回形式,所有工具都通过它返回: | 函数 | 用途 | 形式 | | --- | --- | --- | | `Object(v)` | 单个对象(站点概况、模版详情) | 紧凑 JSON(不缩进、省略空值) | | `List(rows, limit)` | 列表 | `{"total":N,"returned":n,"truncated":bool,"items":[...]}` | | `Table(cols, rows, limit)` | 行多、字段固定的数据 | 首行列名的 TSV 文本,比 JSON 省一半以上字符 | | `Export(name, csv)` | 导出工具(第 5 节) | `EmbeddedResource`(`text/csv`) | | `Fail(err)` / `Upstream(name, err)` | 错误 | `isError` + 一句中文说明 + 处理建议 | 统一限制:单次结果(非导出)不超过 30,000 字符;超过时按行截断并标明 `truncated`,附一句“请缩小范围或使用 xxx_export”。这样不会触发 AgentServer 的 10 万字截断(那条路径会把前 10 万字仍放进上下文)。 所有对象和行在序列化前经过 `utils/pii` 脱敏(规则沿用旧 MCP:用户姓名、电话、邮箱、详细地址等)。 ### 3.4 中间件与运行约束 - 日志:工具名、参数摘要(截断到 2KB)、耗时、结果大小、是否截断、错误;写入独立日志文件,不含调用人。 - 超时:单次调用默认 60 秒(导出工具 300 秒),超时返回 `Upstream` 错误。AgentServer 侧第三方工具超时为 10 分钟,不会先于 Agent MCP 超时。 - 并发:全局信号量(默认 8),保护 FWH 云平台与数据库;排队超过 30 秒返回“服务繁忙”。 - panic 恢复:返回“内部错误”,日志记录堆栈。 ### 3.5 约束测试 `agentmcp_test.go` 注册全部工具集后检查: - 名称匹配 `^[a-z][a-z0-9_]{2,39}$`,且全局唯一。AgentServer 中引擎侧完整名称为 `mcp__gateway__<服务名>__<工具名>`,服务名用 3~6 个字符(`fwh`、`fwhops`)时总长不超过 64; - 每个工具都有 `title`、非空中文描述,并显式声明 `readOnlyHint`;`readonly` 工具集中不得有 `readOnlyHint=false` 的工具,`ops` 工具集中不得有只读工具; - inputSchema 的 `type` 为 `object`; - `kit.List`、`kit.Table` 的截断与 30,000 字符上限。 各领域的映射函数(上游结构 → 精简行)配单测,用固定的上游 JSON 样本。 ## 4. 工具清单(一期,只读) 按领域组织,约 30 个。“来源”列是复用的现有数据访问函数,实现时以代码为准。 | 领域 | 工具 | 说明 | 来源 | | --- | --- | --- | --- | | 站点 | `site_search` | 按 SN 片段、国家、激活状态、版本搜索站点,分页 | `QuerySiteInfoList`、站点信息表 | | | `site_get` | 单站点概况:基本信息、版本、网络、工作模式、当前告警数 | `QuerySiteBaseInfoList`、`QuerySiteModeInfo`、`QuerySiteAlarmList` | | | `site_power_flow` | 单站点单日功率流(5 分钟粒度,`Table`) | `QuerySitePowerFlow` | | | `site_power_flow_export` | 单站点多日功率流导出(≤ 31 天,CSV) | 同上,按天循环 | | 告警 | `alarm_current` | 站点当前告警列表 | `QuerySiteAlarmList` | | | `alarm_events` | 站点一段时间内的告警/事件记录 | `QueryEventOrAlarmListOnTimeRange` | | | `alarm_snapshot` | 站点某日的告警快照 | `QueryAlarmSnapshotInfo`、PG 快照数据 | | | `alarm_stats` | 全量站点的告警统计(按日期、国家汇总) | `QueryAlarmStatistics`、告警统计服务 | | | `alarm_sites_by_code` | 触发某告警码的站点列表 | `QueryAlarmStatisticsFromCode` | | 停机 | `shutdown_stats` | 某日停机统计汇总(不传日期取最新) | 停机统计服务 | | | `shutdown_sites` | 某日停机站点列表 | `QuerySiteShutdownList` | | 日志 | `mqtt_log_search` | 站点 MQTT 日志,按命令类型与时间范围 | `QueryMqttLogListOnTimeRange` | | | `ems_log_records` | EMS 日志召唤记录 | FWH Pro API | | | `update_log_system` | 系统升级记录;传记录 ID 时返回详情 | FWH Pro API | | | `update_log_ahub` | aHub 升级记录 | FWH Pro API | | 故障录波 | `fault_record_list` | 故障录波日志列表 | FWH Pro API | | | `fault_record_detail` | 录波子记录 | FWH Pro API | | | `fault_record_callable` | 设备可召唤的录波列表 | FWH Pro API | | | `fault_record_call_history` | 录波召唤记录 | FWH Pro API | | 调度 | `tou_template_list` | TOU 模版列表,`env=prod/test` | 调度配置服务 | | | `tou_template_get` | TOU 模版详情:按季节 × 日类型整理时段,附调度模式识别结果 | 同上 + 调度配置文档中的解析规则 | | SIM | `sim_get` | 按 ICCID 查询,自动判断平台(SmartHub / Acceleronix) | SIM 卡管理服务 | | | `sim_search` | 按状态、平台、关键字搜索 | 同上 | | | `sim_usage_monthly` | 单卡某月每日用量 | 同上 | | EMS 参数 | `ems_param_lookup` | 按 PID / VID 查询宏名与描述 | emspara | | Plane | `plane_issue_search` | 按关键字、状态、项目搜索工作项(只读) | Plane V1 | | | `plane_issue_get` | 工作项详情(含最近评论) | Plane V1 | | 工具 | `sn_parse` | 解析设备 SN 的含义(型号、产地、批次等) | 旧 `util_parse_sn` 的规则(拷贝) | 一期不提供: - 原始存储访问(Mongo / Redis / PG 任意查询)。确实需要时,二期增加限定集合、限定字段、只读账号、带行数上限的查询工具,或针对具体场景补任务级工具; - 批量下载、站点分析、站点监控配置等与 ToolBox 页面任务绑定的工具(它们的“任务属于谁”在无用户模型下没有意义); - 写工具(第 7 节)。 ## 5. 大数据与文件 ### 5.1 导出工具 需要明细分析的场景(多日功率流、长时间告警明细、批量 SIM 用量)用 `*_export` 工具:服务端分页拉齐数据,生成 CSV,以 `EmbeddedResource`(`text/csv`,URI 如 `fwh://export/site_power_flow/SN123_20261001_20261031.csv`)返回,同时附一段文本摘要(行数、列名、时间范围、前 5 行)。导出上限 20MB,超过时要求缩小范围。 ### 5.2 AgentServer 需要的小改动 ADR-0021 的结果处理(`mcp-gateway/server.ts` 的 `postProcess`)目前:超过 10 万字的文本保存到 `.agent/mcp/` 但前 10 万字仍放进上下文;`resource` 类型的内容原样交给引擎。为了让导出数据只落盘、不进上下文,建议在 AgentServer 增加一条通用规则: - **`resource` 内容一律保存到工作区 `.agent/mcp/`**(文件名取 URI 最后一段,`blob` 解码为二进制,`text` 原样写入),在结果中替换为“已保存到 `<路径>`(大小、MIME)”,不内联内容。 这是通用行为,不含 FWH 逻辑,约 20 行改动加测试。改动之前的过渡办法:导出工具改为返回普通文本并控制在 10 万字以内(约 1,500~3,000 行),超出部分要求分段导出。 ### 5.3 二进制文件 故障录波原始文件、日志包等二进制内容在一期不直接交给 Agent。需要时提供“解析后的结果”工具(如录波通道数据导出为 CSV),由 Agent MCP 在服务端解析;是否需要见 Q-03。上述 5.2 的改动落地后,也可以用 `blob` 资源交付原始文件。 ## 6. 部署与接入 ### 6.1 Agent MCP - systemd 单元 `fwh-agent-mcp.service`:`fwh-agent-mcp -set=readonly -listen=127.0.0.1:8091`,工作目录与主服务相同。 - 独立配置文件 `config.agent-mcp.env`(`-config` 可指定,示例 `deploy/config.agent-mcp.env.example`):`AGENT_MCP_TOKEN`(Bearer 令牌)、`AGENT_MCP_MAX_CONCURRENCY`、`AGENT_MCP_LOG_PATH`,以及工具用到的数据库、Redis、FWH 生产/测试 API、Plane Web。不读主服务的 `config.env` 与旧 MCP 的 `config.mcp.env`,只放自己需要的凭证;文件缺失时拒绝启动。 - 部署脚本:新增 `service/cmd/deploy_agent_mcp.sh`(构建、拷贝、重启),不改动旧 MCP 的 `deploy_mcp.sh`。 - AgentServer 与 ToolBox 不在同一台机器时,监听内网地址,防火墙只放行 AgentServer 主机(见 requirements Q-01)。 ### 6.2 AgentServer 登记 后台“MCP 服务” › 添加: | 项 | 值 | | --- | --- | | 名称 | `fwh` | | 标题 | FWH 业务数据 | | 传输 | 远程 HTTP,`http://127.0.0.1:8091/mcp` | | 请求头 | `Authorization: Bearer ` | | 工具策略 | 全部只读,默认“直接调用” | Profile `fwh-assistant` 勾选 `fwh`。工具使用的业务说明(字段含义、常见排查流程、各工具的配合方式)写在 AgentServer 技能 `fwh-assistant` 中([technical-design.md](technical-design.md) 第 6 节);Gateway 不转发上游的 `instructions`,所以不依赖它。 ## 7. 写工具(二期) 写工具单独部署为第二个 MCP 服务 `fwhops`(`-set=ops`),只勾选进需要它的 Profile(例如只给运维角色用的 Profile)。工具策略保持“跟随权限模式”,Profile 的权限模式为“需审批”,每次调用都弹审批卡片。 候选: | 工具 | 说明 | 注意 | | --- | --- | --- | | `feedback_create` | 在 Plane 创建 Intake Issue | 提交人固定为服务账号“AI 助手”;描述中由 Agent 写明背景。需要真实提交人时由 ToolBox 页面提交,不经过 MCP | | `tou_template_save` | 保存测试服 TOU 模版 | 只允许测试服;写入前按调度配置需求中的规则校验,返回差异 | | `site_info_refresh` | 触发站点信息表更新 | 幂等;限频 | 无用户模型下,写工具的“谁做的”只记录在 ToolBox 会话归属和 AgentServer 审计中,业务系统里看到的是服务账号。如果某个写操作在业务上必须归属到具体的人,就不放进 MCP。 ## 8. 待确认 | 编号 | 问题 | 建议 | | --- | --- | --- | | Q-01 | 一期工具清单(第 4 节)是否覆盖主要使用场景,是否有遗漏的高频查询 | 按旧助手的调用日志统计高频工具补充 | | Q-02 | 是否需要原始存储查询工具(Mongo / PG) | 一期不做;收集到具体场景后补任务级工具 | | Q-03 | 故障录波、日志包等二进制数据是否需要交给 Agent 分析 | 先提供解析后的 CSV;原始文件待 5.2 改动后再评估 | | Q-04 | 5.2 的 AgentServer 改动是否接受 | 建议做,导出类工具依赖它 | | Q-05 | 写工具(第 7 节)是否需要、给哪些角色 | 一期不做;二期先上 `feedback_create` | | Q-06 | 是否需要访问测试服 FWH 平台的工具 | 只在调度模版上区分 `env`;其他工具只查生产服 |