状态:设计初稿,2026-10-24。基于 ToolBox master
a10cbda、AgentServer969a993(ADR-0021 第三方 MCP 服务)。本文描述为新 AI 助手新建的 MCP Server(下称 Agent MCP),与旧助手的service/cmd/mcp无关:旧 MCP 保持现状继续服务 Kiro,新 MCP 不复用其工具代码。
Agent MCP 是 FWH 业务数据的无用户状态的工具服务:
userId、sessionId 参数,不按用户隔离数据,不记录调用人。| 旧 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,工具描述要能自解释 |
readOnlyHint,由测试保证。放在 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 # 写服务(二期)
使用 mcp-go v1.4(go.mod 当前版本)的结构体生成参数 schema(mcp.WithInputSchema[T])和类型化处理函数(mcp.NewTypedToolHandler),参数校验交给 server.WithInputSchemaValidation():
// 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(d.Destructive), // 覆盖、删除类写操作为 true:自动模式下也每次审批
mcp.WithOpenWorldHintAnnotation(false),
),
mcp.NewTypedToolHandler(func(ctx context.Context, _ mcp.CallToolRequest, args T) (*mcp.CallToolResult, error) {
return d.Handler(ctx, args)
}),
)
}
一个工具的写法:
// 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.Properties 对生成的字段 schema 做补充(合并进对应字段,服务端按补充后的 schema 校验);Go 的切片字段默认生成可空类型(null | array),必填数组要用它改为 array。AgentServer 网关只接受 type: object 的 inputSchema,测试中要检查。
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 + 一句中文说明 + 处理建议。Upstream 只返回数据源、错误类别与错误编号,原始错误(可能含内部地址、SQL)只写日志 |
统一限制:单次结果(非导出)不超过 30,000 字符;超过时按行截断并标明 truncated,附一句“请缩小范围或使用 xxx_export”。这样不会触发 AgentServer 的 10 万字截断(那条路径会把前 10 万字仍放进上下文)。
所有对象和行在序列化前经过 utils/pii 脱敏(规则沿用旧 MCP:用户姓名、电话、邮箱、详细地址等)。
Upstream 错误。AgentServer 侧第三方工具超时为 10 分钟,不会先于 Agent MCP 超时。agentmcp_test.go 注册全部工具集后检查:
^[a-z][a-z0-9_]{2,39}$,且全局唯一。AgentServer 中引擎侧完整名称为 mcp__gateway__<服务名>__<工具名>,服务名用 3~6 个字符(fwh、fwhops)时总长不超过 64;title、非空中文描述,并显式声明 readOnlyHint;readonly 工具集中不得有 readOnlyHint=false 的工具,ops 工具集中不得有只读工具;type 为 object;kit.List、kit.Table 的截断与 30,000 字符上限。各领域的映射函数(上游结构 → 精简行)配单测,用固定的上游 JSON 样本。
按领域组织,约 30 个。“来源”列是复用的现有数据访问函数,实现时以代码为准。
| 领域 | 工具 | 说明 | 来源 |
|---|---|---|---|
| 站点 | site_search |
按 SN 片段、国家、激活状态、版本搜索站点,分页 | QuerySiteInfoList、站点信息表 |
site_get |
单站点概况:基本信息、版本、网络、工作模式、当前告警数 | QuerySiteBaseInfoList、QuerySiteModeInfo、QuerySiteAlarmList |
|
site_power_flow |
单站点单日功率流(5 分钟粒度,Table) |
QuerySitePowerFlow |
|
site_power_flow_export |
单站点多日功率流导出(≤ 31 天,CSV) | 同上,按天循环 | |
site_info_sync_status |
站点信息表同步状态:最近一次同步的进度、请求/完成时间,最近一次成功同步的时间与站点数,是否需要重新同步的建议 | GetSiteInfoUpdateNewRecord、GetSiteInfoUpdateLastComplete |
|
| 告警 | 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_devices |
站点可做录波操作的设备(aGate、aPower-PE/MPPT、aHub) | GetFaultRecordSiteDevices |
fault_record_device_info |
读取设备录波信息(故障总体信息、录波功能基本信息、录波总目录);长任务,经主服务 MQTT 收发 | SubmitFaultRecordOperation(device_info) |
|
fault_record_param_read |
读取设备时间、录波开关;长任务 | 同上(param_read) | |
fault_record_operations / fault_record_operation_get |
操作记录、操作结果(逐项解析,不含原始报文) | FaultRecordOperations 集合 |
|
fault_record_data |
解析录波文件:故障信息与波形概要(各通道 min、max、触发点值,不含逐点数据) | MinIO fault-record/ |
|
fault_record_export |
录波 CSV(zip,blob 资源) | 同上 | |
| 故障录波(云平台) | cloud_fault_record_list |
云平台故障录波日志列表 | FWH Pro API |
cloud_fault_record_detail |
录波子记录 | FWH Pro API | |
cloud_fault_record_callable |
设备可召唤的录波列表 | FWH Pro API | |
cloud_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 查询宏名与描述;ids 为十进制整数数组(0~65535,1~50 个,与快照 XID 的 uint16 编号一致),schema 中声明约束由服务端校验 |
emspara |
| Plane | plane_issue_search |
按关键字、状态、项目搜索工作项(只读) | Plane V1 |
plane_issue_get |
工作项详情(含最近评论) | Plane V1 | |
| 工具 | sn_parse |
解析设备 SN 的含义(型号、产地、批次等) | 旧 util_parse_sn 的规则(拷贝) |
一期不提供:
需要明细分析的场景(多日功率流、长时间告警明细、批量 SIM 用量)用 *_export 工具:服务端分页拉齐数据,生成 CSV,以 EmbeddedResource(text/csv,URI 如 fwh://export/site_power_flow/SN123_20261001_20261031.csv)返回,同时附一段文本摘要(行数、列名、时间范围、前 5 行)。导出上限 20MB,超过时要求缩小范围。
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 行),超出部分要求分段导出。
故障录波原始文件、日志包等二进制内容在一期不直接交给 Agent。需要时提供“解析后的结果”工具(如录波通道数据导出为 CSV),由 Agent MCP 在服务端解析;是否需要见 Q-03。上述 5.2 的改动落地后,也可以用 blob 资源交付原始文件。
超过 3 分钟的操作以 MCP 长任务执行(AgentServer 接入指南 §5、docs/design/16-mcp-support.md §3):工具立即返回任务,AgentServer 在后台按 pollInterval 查询,结束后把结果作为新消息发到会话、唤醒模型。不做“提交 + 查询”让模型轮询;查询进度的只读工具保留,供用户主动询问。
任务状态不放在 MCP 进程里。 mcp-go 的任务只存在进程内存,fwh-agent-mcp 重启后平台查询得到“任务不存在”而按失败通知用户,而底层作业其实仍在执行。所以长任务工具用 kit.TaskDef 定义,由 kit.TaskRouter 在 mcp-go 之前处理任务协议:
| 请求 | 处理 |
|---|---|
tools/call 带 task |
Start:启动或挂到已有作业上,立即返回任务(含 pollInterval 与给模型的即时说明 model-immediate-response) |
tasks/get |
Status:读取业务数据(如 Mongo 中的作业记录)翻译为任务状态 |
tasks/result |
任务结束后返回工具结果(失败为 isError),带 related-task |
tasks/cancel |
Cancel:由工具按任务 id 中的标记决定是否真的停止作业 |
| 其他请求、不是本服务签发的任务 id | 原样交给 mcp-go |
fwhtask.<工具>.<业务键>.<标记>.<随机数>.<签名>,签名为以访问令牌为密钥的 HMAC,伪造或篡改的任务 id 一律返回“找不到任务”。task 的普通调用照常由 Def.Handler 同步返回。site_info_refresh 是第一个长任务工具:业务键为同步记录(SiteInfoManageRecord)的 id,标记为本次调用的动作。
| 动作(标记) | 任务 |
|---|---|
triggered(本次触发) |
跟随同步记录:pending / running → working(进度说明“同步中,已进行 N 分钟”);complete → completed(站点数、耗时、完成时间);error → failed;cancelled → cancelled;超过 6 小时未完成 → failed(疑似中断)。取消时停止同步 |
tracking(已有同步在进行) |
同上,但取消只停止跟踪,不停止同步(其他会话可能也在等它) |
skipped_fresh / skipped_stale(未触发) |
立即完成,结果为不触发的原因 |
取消同步由主服务配合:同步循环每页检查 ctx;fwhQueue.CancelSiteInfoUpdate 对执行中的任务发出取消(worker 停止后记为 cancelled),对排队中的直接删除。同步任务超时为 4 小时(SiteInfoUpdateTimeout),超时同样会停止同步并记为失败。
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。后台“MCP 服务” › 添加:
| 项 | 值 |
|---|---|
| 名称 | fwh |
| 标题 | FWH 业务数据 |
| 传输 | 远程 HTTP,http://127.0.0.1:8091/mcp |
| 请求头 | Authorization: Bearer <AGENT_MCP_TOKEN> |
| 工具策略 | 全部只读,默认“直接调用” |
Profile fwh-assistant 勾选 fwh。工具使用的业务说明(字段含义、常见排查流程、各工具的配合方式)写在 AgentServer 技能 fwh-assistant 中(technical-design.md 第 6 节);Gateway 不转发上游的 instructions,所以不依赖它。
写工具单独部署为第二个 MCP 服务 fwhops(-set=ops),只勾选进需要它的 Profile(例如只给运维角色用的 Profile)。工具策略保持“跟随权限模式”,Profile 的权限模式为“需审批”,每次调用都弹审批卡片。
候选:
| 工具 | 说明 | 注意 |
|---|---|---|
feedback_create |
在 Plane 创建 Intake Issue | 提交人固定为服务账号“AI 助手”;描述中由 Agent 写明背景。需要真实提交人时由 ToolBox 页面提交,不经过 MCP |
tou_template_save |
保存测试服 TOU 模版 | 只允许测试服;写入前按调度配置需求中的规则校验,返回差异 |
site_info_refresh |
触发站点信息表同步,以长任务执行(5.4) | 触发前检查同步状态(与 site_info_sync_status 相同的判断):同步中改为跟踪,不重复触发;最近 24 小时内已成功、或超过 6 小时未完成疑似中断时不触发,force=true 可强制。同步结束后平台把结果发给模型。任务投递到主服务的队列执行(MCP 只创建入队客户端与 Inspector,Redis 库号与主服务一致) |
fault_record_fetch |
召唤设备故障录波,长任务 | 经主服务执行器收发 MQTT(MCP 不连 MQTT,只提交与读取 Mongo 中的操作);任务跟随操作记录 |
fault_record_param_write |
设置设备录波参数或执行动作,长任务 | 破坏性(清除波形不可恢复),每次审批 |
fault_record_cancel / fault_record_retry_download |
取消操作;上传完成但下载失败时重试下载 |
无用户模型下,写工具的“谁做的”只记录在 ToolBox 会话归属和 AgentServer 审计中,业务系统里看到的是服务账号。如果某个写操作在业务上必须归属到具体的人,就不放进 MCP。
| 编号 | 问题 | 建议 |
|---|---|---|
| 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;其他工具只查生产服 |