Files
fire-safety-ymd/docs/specs/fire-safety-ymd-superagent-mcp-spatial-readonly-v1.md
T
2026-09-05 19:33:45 +08:00

224 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 森林防火空间只读 MCP v1 Spec
| 项 | 内容 |
| --- | --- |
| 状态 | Implemented;live readiness 与本地 7 tools/call 已通过;同一 SuperAgent 中 `th-hotel-simple-superagent` 已稳定启用/调用并作为兼容档案参照;公网 `/mcp` 已到达 Go,但旧版本门禁曾返回错误,消防服务按 proven-profile 兼容方式部署后仍需完成真实工具回调 |
| 日期 | 2026-09-05 |
| Checkpoint | `fire-safety-ymd-superagent-mcp-proven-profile-compatibility`(基础空间工具契约沿用 v1) |
| 需求来源 | 用户提供 8 张 PostgreSQL/PostGIS 表结构与每表 2 条样例,要求先基于现有数据建设 MCP |
## 1. 背景与已核对输入
`docs/import/db-samples/` 中包含以下表的结构和样例:
- `st_2_mpslfh_t_slfh_syd`:水源地,点。
- `st_2_xianyouxushuichiguan`:蓄水池,点。
- `st_2_xianyoufanghuotongdao`:防火通道,线。
- `st_2_fanghuojianchazhan`:防火检查站,点。
- `st_2_fanghuoliaowangshao`:防火瞭望哨,点。
- `st_2_fanghuowangge`:防火网格,面。
- `st_2_linqugongkuangqiye`:林区工矿企业,面。
- `st_2_mudifenqu_mian`:墓地坟区,面。
SQL 文件仅作为结构和数据契约参考。文件包含 `DROP TABLE` 等导出语句,本项目不会执行这些文件。
当前样例足以确定首版字段映射,但不能证明全库的数据完整性、坐标系、几何类型一致性、索引、时效或资源当前可用性。尤其是:
- 所有 `geom` 列均声明为宽泛的 `geometry(GEOMETRY)`,没有 typmod SRID。
- 现场全量导入进一步确认:`st_2_xianyoufanghuotongdao.geom` 的源记录混合二维与 Z 维度,原 `geometry(GEOMETRY)` 二维 typmod 会拒绝 Z;用户改用裸 `geometry` 保留原始维度后报告重导成功。该导入反馈仍需只读 probe 核验最终数量和分布。
- 样例几何十六进制是未携带 SRID 的 WKB;坐标数值及水源表独立经纬度字段看起来符合 WGS84,但这只能作为待验证线索。
- 样例 DDL 只有防火网格明确包含 GiST 几何索引。
- 多张表含姓名、电话等受限字段;首版 MCP 不返回这些字段。
- 用户提供的 SuperAgent 现场截图显示公网 `/mcp` 请求已经到达 Go 服务,Bearer、`Content-Type` 和 JSON-RPC 前置校验通过,随后旧版本门禁返回错误;截图没有捕获客户端版本字段是否存在、类型和值。同一 SuperAgent 中 `th-hotel-simple-superagent` 已稳定启用/调用,这是兼容档案的现场参照,但消防 MCP 尚未完成公网数据库工具调用。
## 2. 目标
- 在现有 Go HTTP 服务内提供默认关闭的 `POST /mcp`。
- 与不能配置协议版本的 SuperAgent 客户端保持兼容:服务端固定返回 MCP `2025-06-18`,对齐已稳定接通的 `th-hotel-simple-superagent` compatibility profile;initialize 中的版本字段和后续协议 Header 都不作为拒绝门禁。使用 JSON-RPC 2.0 和单请求 JSON 响应,目标是打通工具调用而不是追求最新协议。
- 通过独立 Bearer Token 鉴权;不得复用 SuperAgent Open API Key。
- 只提供固定、参数化、只读、按服务端可信数据范围执行的空间查询工具;默认使用镇街白名单,数据库全范围必须显式启用。
- 支持在现有业务记录中按地名搜索有界候选,让没有坐标的用户先确认一个候选,再进入距离和包含关系查询。
- 使用 PostgreSQL/PostGIS 作为事实来源,并在启用 MCP 前校验 PostGIS、表、SRID、几何类型和有效性。
- 工具结果明确表达数据来源、生成时间、限制与需要现场确认的事项。
- 提供一个只输出 schema/几何统计、不输出业务记录或联系人数据的 PostGIS readiness probe。
## 3. 非目标
- 不开放任意 SQL、任意表查询、文件读取或通用 HTTP 代理。
- 不写数据库,不调度人员或资源,不生成/下发指挥命令。
- 不声称防火通道是可通行路线,不实现路网拓扑、最短路、坡度或实时封路计算。
- 不声称网格队伍的实时位置或集结点;现有表只提供责任队伍和区域信息。
- 不依据资源记录存在就宣称水源、检查站或瞭望哨当前可用。
- 不返回负责人、队长、值班人员、书记、联系电话或图片地址。
- 不提供任意地址、山名或道路的外部地理编码,不实现完整地名库、拼音/别名纠错,也不自动选定地名候选。
- 不在本 checkpoint 内建立最终用户身份、动态角色或多租户模型;首版使用每环境 MCP 凭证和服务端静态数据库全范围/镇街白名单。
## 4. MCP 传输与安全契约
### 4.1 支持的方法
- `initialize`
- `notifications/initialized`
- `tools/list`
- `tools/call`
服务端固定返回协议版本标识 `2025-06-18` 和 `tools` capability。首版是无会话、非流式的 Streamable HTTP 子集:`POST` 返回 `application/json`;`GET /mcp` 返回 `405 Method Not Allowed`。
#### SuperAgent compatibility profile
服务端固定返回 `result.protocolVersion=2025-06-18`,但不把客户端版本字段当作版本协商或拒绝依据:可解析的 JSON-RPC `initialize` 中,`params.protocolVersion` 可以缺失、为空、为非字符串或为其他值,均不单独触发版本错误。`MCP-Protocol-Version` Header 可以缺失或携带任意值,也不作为版本拒绝门禁;SuperAgent 配置中不需要填写版本字段或 Header。
固定返回 `2025-06-18` 不表示服务实现或声明支持客户端填写的任意版本,也不把 `2025-03-26`、`2025-11-25` 等值列为实现目标。该 profile 只为兼容已稳定接通的 SuperAgent 客户端,版本字段/Header 之外的 HTTP 方法、JSON-RPC 结构、鉴权、Origin、Content-Type、工具 schema、只读数据范围和敏感字段边界仍严格执行。
initialize 日志只记录 `direct_success`(客户端值为 `2025-06-18`)或 `compatibility_success`(版本字段缺失或其他值被兼容处理),不记录客户端原始版本值。
### 4.2 入站控制
- MCP 默认关闭,关闭时不注册 `/mcp`。
- 只接受 `Content-Type: application/json`。
- 使用 `Authorization: Bearer <FIRE_SAFETY_MCP_AUTH_TOKEN>`,常量时间比较。
- Token 至少 32 个可打印 ASCII 字符,且不能等于 SuperAgent Open API Key。
- 初始化响应固定返回 `protocolVersion: 2025-06-18`;请求 body 中的 `protocolVersion` 以及 `MCP-Protocol-Version` Header 均为兼容元数据,不作为拒绝门禁。SuperAgent 配置不需要增加版本字段或 Header。
- 请求体默认最大 256 KiB,最大可配置 1 MiB。
- 首版只接受服务到服务请求;带非空 `Origin` 的请求拒绝,避免浏览器和 DNS rebinding 风险。
- 不接受模型提供的用户、角色、租户或授权范围。范围模式和可选镇街列表只来自服务端配置。
- 每次工具调用有硬超时;日志只记录 request ID、方法/工具名、耗时和结果类别,不记录参数、坐标、Token、SQL 或结果正文。
### 4.3 服务端数据范围
- `FIRE_SAFETY_MCP_SCOPE_MODE=town_allowlist` 是默认值;启用 MCP 时要求 `FIRE_SAFETY_MCP_ALLOWED_TOWNS` 非空,并按数据库镇街字段精确过滤。
- `FIRE_SAFETY_MCP_SCOPE_MODE=all` 必须显式配置;它授权查询当前配置数据库中 MCP 固定查询表的全部镇街以及镇街字段为空的记录,此时 `FIRE_SAFETY_MCP_ALLOWED_TOWNS` 必须为空。
- `all` 不会放宽只读、固定 SQL、空间半径、结果数量、字段脱敏、SRID 或 readiness 限制。
- 业务表中的镇街值不能自动决定授权范围;范围只能由可信服务端配置建立。
## 5. MCP 工具
除地名候选工具外,所有工具的坐标参数使用 WGS84:`longitude` 范围 `[-180, 180]`,`latitude` 范围 `[-90, 90]`。需要距离的工具接收 `radius_meters` 和 `limit`,同时有工具级默认值和硬上限。
### 5.1 `fire_safety_search_place_candidates`
输入 `place_name`(去除首尾空白后 2 至 100 字符)和可选 `limit`(默认 10、最大 20),在 8 张现有业务表的名称、镇街和村庄字段中做不区分大小写的文字包含匹配。百分号和下划线按普通文字处理,不作为 SQL 通配符。
返回稳定资源类型、记录 ID、名称、镇街、村庄、命中字段、`exact | partial`、WGS84 坐标和 `location_kind`:
- 点记录返回 `recorded_point`。
- 线记录返回首个组成线的起点,面记录返回 `ST_PointOnSurface` 代表点,两者标记为 `representative_point`。
候选不等于用户位置。无结果时 Agent 必须请用户补充名称或地图选点;多结果时必须请用户选择;即使只有一个候选,也要先回显确认,不能直接调用后续距离工具。该工具不返回联系人字段,不调用外部地图服务。
### 5.2 `fire_safety_resolve_incident_context`
输入演练点坐标,查询覆盖该点的防火网格,返回网格 ID、镇街、区域标签。用于建立后续查询上下文,不返回个人信息。
### 5.3 `fire_safety_find_nearby_water_sources`
合并水源地和蓄水池,按球面距离返回候选水源。返回名称/标签、类型、镇街、村、坐标、距离、容量(数据存在时)、源表报告状态和未经解释的源时间原值(数据存在时)。结果固定标记“当前可用性未验证”;时间原值的单位和时区待数据所有者确认。
### 5.4 `fire_safety_find_command_post_candidates`
合并防火检查站和防火瞭望哨,返回附近候选设施、坐标、距离及源表报告状态。结果只代表空间候选,必须由现场核验安全、通信、容量、可达性和火势上风向等条件。
### 5.5 `fire_safety_list_nearby_access_lines`
返回附近防火通道名称、按 WGS84 geometry 计算的米制长度、最近接入点和未经解释的源更新时间原值。该工具不称为 route planner;它不返回“推荐路线”或“可通行”结论。
### 5.6 `fire_safety_get_responsible_units`
根据坐标查询覆盖网格及其防火中队名称。明确返回 `live_location_available=false` 和 `assembly_site_available=false`,因为当前表没有队伍实时位置、战备状态或正式集结点字段。
### 5.7 `fire_safety_find_nearby_risk_areas`
合并墓地坟区和林区工矿企业,返回点位周边风险区域类别、名称/标签、镇街、村、方位、是否覆盖演练点和距离。联系人字段不返回。
## 6. 统一结果语义
工具成功结果使用:
```json
{
"status": "ok | no_results",
"data": {},
"metadata": {
"generated_at": "RFC3339 UTC",
"data_sources": [],
"spatial_reference": "EPSG:4326",
"result_count": 0
},
"warnings": []
}
```
`structuredContent` 保存上述对象,同时在 `content[0].text` 返回同一对象的 JSON 文本,兼容只读取文本内容的 MCP 客户端。
`data_sources` 只使用稳定领域名称(如 `water_source`、`fire_grid`),不向模型暴露历史物理表名。
`warnings` 使用 `results_limited_to_server_authorized_towns` 表示镇街白名单模式,使用 `results_include_all_towns_in_configured_database` 表示显式数据库全范围模式,避免 Agent 混淆结果范围。所有工具同时返回 `source_records_with_invalid_geometries_are_excluded`,防止 Agent 把排除后的无结果解释成数据库确认不存在相关记录。
工具级失败仍使用 JSON-RPC 成功响应中的 `isError=true`,结构化错误只公开稳定错误码:
- `INVALID_ARGUMENT`
- `TOOL_NOT_FOUND`
- `DATA_SOURCE_UNAVAILABLE`
- `QUERY_TIMEOUT`
- `INTERNAL_ERROR`
数据库 DSN、SQL、表结构细节和原始驱动错误不得进入 MCP 响应。
## 7. PostgreSQL/PostGIS 契约
- 使用 `pgx/v5` 原生连接池;当前仅面向 PostgreSQL,不引入 ORM。
- 连接设置 `default_transaction_read_only=on`、`statement_timeout` 和应用名。
- SQL 固定在 Repository,所有值使用 `$n` 参数;只有代码内表白名单可参与标识符拼接。
- 查询始终应用服务端范围、半径和返回数量上限;`town_allowlist` 使用参数化镇街数组,`all` 使用服务端布尔参数显式跳过镇街条件,模型不能提供该参数。
- 地名查询使用参数化的文字包含匹配,先按精确名称、精确村庄、精确镇街,再按前缀和普通包含排序;不拼接用户输入。线面候选的二维代表点只在只读查询中计算,不修改原始几何。
- MCP v1 只支持已实库确认的 EPSG:4326。启用 MCP 时 `FIRE_SAFETY_POSTGIS_EXPECTED_SRID` 必须显式配置为 `4326`。
- readiness 校验要求每张已用表存在、PostGIS 可用、非空几何的 SRID 均为 4326、类型符合点/线/面预期且坐标不越过 WGS84 范围;表/SRID/类型/越界不符合时服务拒绝启用 MCP。
- 无效几何、空几何、空表或缺少空间索引作为 readiness warning。固定查询使用 `ST_IsValid` 排除无效几何,不自动调用 `ST_MakeValid`,不修改原始事实;生产前应评估数据缺口并补齐查询表达式适用的 GiST 索引。
- 原始防火通道列保持裸 `geometry` 以容纳混合二维/Z。不得用 `ST_Force2D` 或重写 WKB 修改原始数据;确需二维投影时仅在只读查询/派生层显式执行并测试其结果语义。
- 不自动执行样例 SQL、SRID 修复或索引 DDL。
## 8. 配置
| 环境变量 | 默认值 | 启用时要求 |
| --- | --- | --- |
| `FIRE_SAFETY_MCP_ENABLED` | `false` | 显式为 `true` |
| `FIRE_SAFETY_MCP_AUTH_TOKEN` | 空 | 必填,独立高熵 Token |
| `FIRE_SAFETY_MCP_SCOPE_MODE` | `town_allowlist` | `town_allowlist` 或 `all` |
| `FIRE_SAFETY_MCP_ALLOWED_TOWNS` | 空 | `town_allowlist` 时必填;`all` 时必须为空 |
| `FIRE_SAFETY_MCP_MAX_BODY_BYTES` | `262144` | `1..1048576` |
| `FIRE_SAFETY_MCP_TOOL_TIMEOUT` | `5s` | `>0` 且不超过 `30s` |
| `FIRE_SAFETY_POSTGIS_ENABLED` | `false` | MCP 启用时必须为 `true` |
| `FIRE_SAFETY_POSTGIS_DSN` | 空 | PostGIS 启用时必填;Secret |
| `FIRE_SAFETY_POSTGIS_EXPECTED_SRID` | 空/`0` | MCP 启用时必须显式为 `4326` |
| `FIRE_SAFETY_POSTGIS_CONNECT_TIMEOUT` | `5s` | `>0` 且不超过 `30s` |
| `FIRE_SAFETY_POSTGIS_QUERY_TIMEOUT` | `3s` | `>0` 且不超过 `30s` |
| `FIRE_SAFETY_POSTGIS_MAX_CONNS` | `4` | `1..20` |
## 9. 验收标准
- MCP 关闭时 `/mcp` 不暴露,健康检查保持可用。
- MCP 启用但 Token、合法范围、PostGIS 或显式 SRID 缺失时配置加载失败;`all` 与非空镇街列表同时出现时失败,错误不含 Secret。
- 无 Token、错误 Token、错误 Content-Type、非空 Origin、超大 body 和非法 JSON 均被稳定拒绝。
- `initialize`、initialized notification、`tools/list` 和 7 个 `tools/call` 契约通过本地测试。
- `initialize` 对客户端版本字段缺失、空值、非字符串、`2025-06-18` 或其他值均固定返回 `2025-06-18`;版本字段和 `MCP-Protocol-Version` Header 不作为拒绝门禁。服务端不宣称支持任意其他版本,也不追求最新版本;日志按 `direct_success`/`compatibility_success` 分类且不包含原始版本值。
- 真实 SuperAgent 验收必须在部署后依次出现 `initialize` → `notifications/initialized`(HTTP 202)→ `tools/list`(7 个固定工具)→ 至少一个受控只读 `tools/call`;只有 initialize 成功不能宣称 MCP 已接通。
- 工具 schema 限制地名长度、经纬度、半径、数量和未知字段;工具不接受授权身份参数。
- Service 测试证明可信数据库全范围/镇街白名单来自构造时配置,并覆盖互斥校验、无结果、超时和仓储失败。
- Repository 只使用参数化值,MCP 结果不包含联系人字段。
- readiness 测试证明无效几何不阻塞启动、会产生排除 warning,且全部固定查询显式包含 `ST_IsValid`;SRID、类型和越界仍为硬失败。
- 地名 Service/Repository 测试覆盖首尾空白、长度、控制字符、结果上限、服务端范围、8 张固定表和候选确认 warning。
- readiness probe 不输出业务行、电话、负责人、DSN 或 SQL。
- `gofmt`、`go test -count=1 ./...`、`go test -race -count=1 ./...` 和 `go vet ./...` 通过。
## 10. 上线前未确认项
- 实库源 CRS 已由数据提供方确认为 EPSG:4326 且无偏移;2026-09-05 已通过单独审核、显式确认和单事务迁移为 4,048 条非空几何补齐 SRID 4326,严格 readiness 已通过。重新导入无 SRID 的原始 SQL 时仍必须重新经过该流程,应用查询不得静默赋值。
- 实库 35 条无效面几何按首版决策排除;需评估由此造成的网格、责任单位和风险区域覆盖缺口是否满足生产要求。
- 各资源状态字段的枚举、更新时间含义和数据刷新责任人。
- MCP 回调网络地址、TLS、SuperAgent Token 轮换方式及真实工具权限。
- SuperAgent 无法配置协议版本;proven-profile 兼容响应、`notifications/initialized`、`tools/list` 和真实 `tools/call` 仍待目标机重建后确认。是否发送 `MCP-Protocol-Version` 不影响版本兼容验收。
- 最终用户身份、区域权限、精确位置权限和审计保留策略。
- 真实数据量下地名重复率、字段质量和无索引包含搜索性能;后续是否引入标准地名表、别名词典、`pg_trgm` 或经审批的外部地理编码服务。
- 路线规划需要的路网拓扑、路面/宽度/坡度/车辆限制、实时封路、火场和天气数据。
- 队伍集结需要的正式集结点、实时位置、战备状态、装备和容量数据。