Add project-bound OAuth MCP queries and controlled analysis with scoped evidence, limits, timeouts and a nearby-model spatial query using EPSG:4326 inputs. Default nearby searches to 200 meters and five results. Integrate investigation messaging/presentation, health/readiness and typed history; remove duplicate web search and fix online simulation and compressed baseline writes. Include project migrations 023-025; keep local credentials and demo seeding excluded. Validation: 319 focused pytest checks; real PostGIS temporary-table regression; read-only Jinqian bridge radius comparison; cross-language MCP discovery/calls.
36 KiB
NextTJWater MCP 查询与认证设计
日期:2026-10-15。状态:查询与认证代码已在本地实现,认证发现、resource 参数和版本头体校验已补齐,默认关闭,未部署;真实 Keycloak 与项目数据库验收待完成。
本设计将现有后端查询能力通过 MCP 开放给普通项目 Agent、调查主 Agent 和调查只读子 Agent。采用 MCP 2026-07-28、Streamable HTTP、现有 Python Server 内挂载服务、可信 Agent 宿主按请求代理认证。旧 CLI 用于查找功能和接口线索,业务实现继续由当前 Server 负责。
第一版完成查询、权限、取消、分页、调查证据和凭据续期的完整闭环。已有报告发布、工单审批、设备控制、调查运行生命周期继续使用当前受控工具;不通过 MCP 自动开放旧 CLI 中的分析执行命令。
当前系统与功能核对
Agent 的独立只读查询现统一通过原生 mcp_tjwater 连接调用。重复的项目查询插件与自定义 HTTP 查询桥已移除;memory_manager、skill_manager 保留完整读写插件,发布、动作、消息发送和界面展示也保留插件形式。普通会话凭据保存在宿主的 src/product/credential-refresh.ts;调查由 worker 以服务身份投递并续期。调查子 Agent 通过父任务绑定访问数据,不持有独立产品凭据。
实施前从 app.main.app.openapi() 核对了 266 路径的契约,其路径集合及各 HTTP operation 与工作区 contracts/server-v1.openapi.json 一致。公开 API 经 app/api/v1/rest_router.py 适配,项目字段注入和分页须看最终注册契约,不能只看底层 handler 签名。旧 CLI 的 41 条业务命令中,38 条能匹配当前 HTTP 方法与路径:29 条读命令、9 条执行命令。另有 3 条需要替换或不开放,详见附表。路径匹配不代表参数兼容、数据可用或算法已完成业务验收。
实施前 requirements.txt 和 conda 环境均为 mcp==1.9.4、fastmcp==2.9.2,当时没有 Server MCP 注册实现。Agent 使用的 OpenCode 2.0.27 配置类型支持 legacy、auto、2026-07-28,宿主认证代理现已补充,见实施说明。
协议选择
官方 specification/latest 指向 MCP 2026-07-28。新实现按该正式版本建设;产品内部连接固定要求这个版本,版本不支持时返回明确错误。第一版不提供旧 initialize 握手兼容层。
| 协议要求 | 本系统设计 |
|---|---|
| 无状态请求与逐请求能力声明 | 每次请求发送标准 _meta 中的版本、客户端信息和能力;不以连接识别用户或调查轮次 |
server/discover |
返回版本、服务信息和实际支持能力;不能用自报服务名建立信任 |
| HTTP 请求镜像头 | 由 SDK发送并校验 MCP-Protocol-Version、Mcp-Method、适用时的 Mcp-Name;头与正文不一致拒绝 |
| 普通结果 | 返回 resultType=complete、structuredContent、匹配的 outputSchema,以及简短文字说明 |
| 列表和资源缓存提示 | 返回 ttlMs、cacheScope;缓存不作为权限证明 |
| 取消与断线 | 取消传到 SDK、数据库和外部请求;断开的流不按旧 SSE 游标恢复 |
这些改变来自正式版变更说明和 Streamable HTTP 规范。协议支持情况以实际发现与调用验收为准,不以软件版本字符串代替。
第一版声明 tools 和 resources;不声明 prompts、subscriptions、MRTR、Tasks、MCP Apps、Sampling、Roots 或 Logging 能力。查询不要求模型补填身份、密码或令牌。现有产品调查任务不会替换为 MCP Tasks。
Schema 使用 JSON Schema 2020-12,模型输入对象拒绝未声明字段;只使用服务自带的本地定义,不在校验时从任意外部 URL 获取 $ref。宿主校验模型投影,Server 再校验完整线协议参数。工具发现、结构化结果、状态句柄和执行错误遵循正式工具规范。
| 失败类型 | 响应与宿主处理 |
|---|---|
| 未认证或过期 | HTTP 401 和 Bearer challenge;宿主最多续期一次 |
| scope 或产品权限不足 | HTTP 403;scope 不足时带 insufficient_scope,不自动扩大产品权限 |
| 请求头与正文不一致 | HTTP 400、JSON-RPC -32020 |
| 协议版本不支持 | HTTP 400、JSON-RPC -32022,列出支持版本 |
| RPC 方法不存在 | HTTP 404、JSON-RPC -32601 |
| 工具不存在或 RPC 参数结构无效 | JSON-RPC -32602 |
| 业务参数、数据源或上下文失败 | resultType=complete、isError=true;返回稳定业务错误码和可操作说明 |
旧客户端 GET/DELETE 到 /mcp 返回 405;旧 Mcp-Session-Id 和 Last-Event-ID 不建立或恢复身份。产品上下文删除使用独立 REST 控制路径,不混用协议会话删除。
server/discover 和固定查询目录按工具名稳定排序,缓存提示取 ttlMs=300000、cacheScope=public,正文仅包含通用定义。业务结果资源取 ttlMs=0、cacheScope=private;宿主按令牌、主体、项目和上下文分别保存缓存,不跨授权上下文复用。缓存规范
服务与调用结构
在现有 FastAPI Server 挂载规范路径 /mcp,通过官方 Python SDK v2 承担协议处理。ASGI 的生命周期纳入现有应用启动和关闭;不得用根路径挂载遮蔽 REST 路由。官方 SDK 支持在现有 ASGI 应用中挂载,其协议版本说明覆盖 2026-07-28。
查询适配器复用现有 service、repository、项目路由和业务权限,不从 MCP 向自己的 REST API再发一次 HTTP 请求,不转发收到的 MCP Bearer。需要从 endpoint 提取逻辑时,提取供 REST 和 MCP 共用的 service,而不直接调用带 FastAPI Depends 默认值的 endpoint 函数。
模型调用查询工具
→ OpenCode 原生 MCP(tools/call,_meta 中关联原生 sessionID)
→ Agent 本地 MCP 服务(内部密钥认证、宿主会话和直接父子关系检查)
→ 宿主专用产品 MCP 客户端
→ Keycloak(按主体获取面向 MCP 的令牌)
→ Server /mcp(标准 MCP 请求 + 宿主绑定的上下文)
→ 项目与任务权限检查
→ 共用查询 service
→ BizDB / TimescaleDB / 既有外围适配器
→ 调查证据保存和有范围的数据结果
OpenCode 原生发现本地 MCP 的工具目录,工具 ID 为 mcp_tjwater_<name>。本地 MCP 默认提供地址解析、会话搜索、调查任务列表与详情、消息读取、待办读取六项查询;启用产品 MCP 后,宿主从真实 Server tools/list 发现24项项目查询,投影并合并到本地目录,共30项。公开网页搜索使用 OpenCode 原生 websearch,Server 不提供搜索接口。可信插件只配置连接和权限策略,不注册独立查询工具或自定义执行器。待办读取经宿主 RPC 访问原插件存储,保留已有清单。
本地服务使用官方 TypeScript Server SDK 2.3.1、2026-07-28 Streamable HTTP,绑定127.0.0.1临时端口,拒绝浏览器来源和错误内部密钥。OpenCode 的 _meta["ai.opencode/sessionID"] 仅是关联值;通过内部通道认证后,服务再解析宿主身份或已核验的直接只读子会话。未知会话、个人项目查询和学习复盘均拒绝。URL、身份、项目、网络、产品认证头和上下文句柄不属于模型参数。产品 MCP 未启用时项目查询不可用,没有旧桥回退。取消信号贯穿整个调用链。OpenCode MCP 会话上下文
现有 OpenCode 共享 MCP 配置缺少逐会话 OAuth 提供器,不能把一份用户 Bearer 填进 mcp.servers.headers。产品内部连接由 Agent 宿主按请求认证,外部 MCP 继续保持原配置路径和现有策略,二者不能误注册为同名工具或形成两份不同凭据的业务连接。
认证与上下文绑定
OAuth 资源与现有登录
Keycloak 继续作为授权服务器。MCP 是独立的受保护资源,资源标识由 TJWATER_MCP_RESOURCE_URI 指定为部署的规范 /mcp URI;令牌目标 audience 取专用客户端 tjwater-mcp。Agent 宿主使用预注册 confidential client tjwater-agent-mcp。用户继续使用当前前端标准流程和 S256 PKCE 登录,不新增产品内的第二次登录。
OAuth scope 分成 mcp.catalog.read、mcp.query 和 mcp.context.bind。宿主启动时以自身 client_credentials 取得仅含 catalog scope 的令牌,发现真实目录;该身份不获得项目业务读取能力。会话查询令牌必须具有 query scope,绑定接口还要求 context.bind。scope 只开放接口类别,具体数据权限仍由产品主体、项目成员关系和工具 registry 求交。
MCP 端提供 /.well-known/oauth-protected-resource/mcp,401 响应包含对应的 WWW-Authenticate 资源发现信息。验证签名、固定 issuer、audience、exp、iat、客户端来源和查询 scope;对新 MCP 使用独立验证器,不依赖当前 REST 验证器可为空的 audience,也不通过修改全局 audience 破坏现有 REST。
标准 OAuth 客户端在授权和 token 请求中携带规范资源 URI。MCP 仅接收确实面向它发行的访问令牌,令牌每次通过 Authorization 头发送,不出现在 URL、工具参数或模型文本中。MCP 授权规范、授权服务器发现
产品宿主先发送无凭据的标准发现请求,从401 challenge 读取资源元数据地址;无地址时按 MCP 路径、根路径依次发现 well-known。资源 URI 必须匹配已配置资源,authorization_servers 必须包含预注册凭据所属的固定 issuer。随后使用官方 SDK 按规范顺序发现 OAuth/OIDC 元数据,严格比较 issuer,从元数据取得 token endpoint;不向其他 issuer 发送原凭据。公开发现请求不带产品 Bearer 或 secret,单份文档最多64KiB,总预算10秒;拒绝重定向和远程明文地址。发现缓存最多5分钟,MCP 401 后失效重查。根据元数据使用 client_secret_post 或 client_secret_basic。
产品内部宿主基于当前产品凭据使用 Keycloak Standard Token Exchange V2 换取面向 tjwater-mcp 的短期访问令牌;保留原主体,限定查询 scope,避免共享一个超级服务账号访问所有普通用户数据。专用令牌只保存在 Agent 主进程内存中,刷新令牌仍由现有登录机制持有。
Keycloak V2 需要 confidential requester、启用 Standard token exchange、subject token 对 requester 的 audience,以及目标 audience/scope 配置。client_credentials 与 token exchange 均发送 resource=规范MCP URI;exchange 另保留产品要求的 audience。身份平台必须将该资源映射到专用 audience,并核验两种 grant 返回的令牌。若部署版本拒绝 resource,应先升级或提供兼容适配再启用;不能删除标准参数后重试。不增加第三方客户端授权码流程。Keycloak Token Exchange
上述 realm/client 能力尚未在线核验,是部署前提,由身份平台配置负责人落实。达不到前提时停止启用内部 MCP;不以普通 API Bearer 转发、共享管理员 token 或关闭 audience 校验降级。
每次调用绑定项目与调查轮次
OAuth 令牌只证明主体与可用 scope,业务范围由 Server 签发的 context_handle 约束。它是应用上下文,不是 MCP 协议会话,也不能单独作为认证凭据。
可信 Agent 宿主通过受认证的 POST /api/v1/mcp-query-contexts 申请上下文。请求中的 session、purpose、项目和任务信息均由原生会话及宿主已有绑定读取;仅允许 azp=tjwater-agent-mcp 的宿主客户端创建任务上下文。Server 从令牌确定主体、从成员关系确定项目读取权限,验证 task、run、execution_request 和父会话占用的租约;原生父子关系由可信宿主先核验;不接受自报用户、权限或数据 scope。
Server 保存的绑定字段包括:主体、client ID、project ID、native session ID、purpose、可用查询工具、任务及运行标识、父会话、创建与过期时间、撤销状态。句柄使用至少 256 位随机值,10 分钟过期。普通上下文到期由宿主重新申请;任务上下文有效性另外取决于当前轮次和 lease,不能用续租让旧轮次重新获得访问权。
所有业务 MCP tool 的线协议 schema 具有宿主字段 context_handle。Agent 宿主客户端在发布给本地 MCP 的 schema 投影中移除它,并在请求 Server 时覆盖注入;它不使用 x-mcp-header,也不进入聊天 transcript 或普通日志。若输入包含同名字段、身份字段或保留 scope 字段,宿主直接拒绝。resources/read 由宿主在应用命名空间 _meta 中注入同一上下文,URI 仅标识任务证据而不携带句柄或令牌。外部客户端若将来接入,应自己调用绑定接口并在宿主侧选择项目,不从模型内容决定项目。
Server 每次同时核验 Bearer 与句柄所属的 sub/azp,重新校验成员关系和当前权限;任务调用还检查项目、任务、run、execution request 和 lease。句柄复制到其他用户、客户端或项目无效。切换项目新建会话,关闭/删除会话撤销绑定;调用 DELETE /api/v1/mcp-query-contexts/{context_handle} 时检查绑定所有者。令牌在新 scope 下续期时不会自动扩大已有上下文的工具授权。
普通会话的 purpose=ordinary_chat,仅访问其绑定项目。调查主 Agent 的 purpose=task_execution,使用 worker 的现有服务主体和本轮任务范围。只读子 Agent 的 purpose=task_reader,宿主根据已经验证的父子关系绑定父轮次;绑定只允许查询和系统证据追加,不授予报告发布、审批、设备控制、Shell 或再次委派权限。个人会话不创建项目查询上下文,返回现有 PROJECT_CONTEXT_REQUIRED。
续期和退出
普通项目会话复用现有 credential_refresh_required 回传与前端 Keycloak updateToken。收到更新的产品 access token 后,宿主重新 exchange 并继续原查询,用户只在现有登录本身失效时重新登录。并发续期按主体与原凭据版本合并,最终缓存按 issuer、resource、主体、scope 和原凭据版本隔离。
后台调查不依赖浏览器。worker 保持当前 client_credentials 续期;新服务 Bearer 到达后由 Agent 更新原会话产品凭据,再 exchange。MCP 声明官方 io.modelcontextprotocol/oauth-client-credentials 授权扩展以支持服务身份;首次范围仍由服务账号成员关系与任务授权决定。Client Credentials 扩展
MCP 401 清除本主体缓存并续期一次;403 不反复刷新、不自动申请更大 scope。认证服务不可用时返回认证暂不可用,不能返回空数组掩盖故障。注销时撤销产品绑定、取消调用并清除宿主缓存。JWT exchange 的源 token 撤销不必然撤销已交换 token,因此 MCP 令牌短期有效,Server 的绑定撤销和每次业务权限检查仍然必须执行。
查询工具目录
以下为第一版明确目录。名称为 MCP 服务端名字,OpenCode 原生发现后添加 mcp_tjwater_ 前缀。元素枚举固定为 junction、pipe、valve、pump、tank、reservoir;时间必须带时区;身份、项目和 network 都不在模型输入中。HTTP 路径表示现有能力来源,MCP 实现调用其共用 service。
| 工具 | 业务输入 | 当前能力来源与限制 |
|---|---|---|
get_element_properties |
element_type、element_id | 六类 /{type}s/properties |
list_elements |
element_type、limit、offset | 六类 /{type}s;返回一页,不暗中获取全网 |
query_nearby_elements |
center、可选 element_type、radius_m、limit、offset | 当前项目模型管网空间邻近查询;直接接收地理编码的 EPSG:4326 [经度,纬度],默认pipe、200米、5条,最大10000米、单页100条;返回element_id、distance_m及properties,不查询原始GIS源要素 |
get_network_options |
kind、可选 pump_id | network-options 及 time、energy、pump-energy;泵能源必须给泵 ID |
get_network_schema |
kind | 固定 network-schemas 枚举,以当前注册路径为准 |
list_scada_devices |
limit、offset | /scada-devices,显式分页 |
get_scada_device |
device_id | /scada-devices/{device_id} |
query_scada_readings |
device_ids、start、end、field | scada-readings/fields;区分 monitored_value 和 cleaned_value |
query_model_baseline_series |
element_type、start、end、元素筛选 | realtime/links、realtime/nodes;标记为模型基线,不称为实测 |
get_model_baseline_at_time |
element_type、element_id、time | realtime/simulation-results |
query_model_baseline_field |
element_type、field、time | realtime/records;只允许当前字段枚举 |
query_analysis_series |
run_id、element_type、element_id、field、start、end | analysis/runs/{run_id}/links 或 nodes/{id} |
query_analysis_values |
run_id、element_type、field、time | analysis/runs/{run_id}/values |
list_analysis_runs |
limit、offset | /analysis/runs |
get_analysis_run |
run_id | /analysis/runs/{run_id} |
list_analysis_results |
run_id、result_type、limit、offset | /analysis/runs/{run_id}/results;保留 result_type |
list_sensor_placement_runs |
limit、offset | sensor-placement-runs;公开 REST 已包装分页,但底层 service 读取整数组,MCP 应推动共用 service 做真实有界查询 |
get_sensor_placement_run |
run_id | sensor-placement-runs/{run_id};REST 适配层已注入 network,MCP 共用 service 也从可信项目上下文解析 |
query_element_history |
elements、start、end、mode、可选 run_id | element-history/query;observed、realtime_comparison、analysis_comparison |
get_element_gis_properties |
element_type、element_id | elements/{type}/{id}/gis-properties |
get_source_gis_properties |
layer_key、source_fid | source-gis/layers/{key}/features/{id}/properties |
get_workbench_runtime |
已有运行态查询参数 | workbench/runtime;只读取 |
get_task_context |
section、data_path、limit、offset、可选 report_revision;无模型可选 task_id | 当前绑定任务的固定摘要及分章节、路径分页详情;预览不等于完整报告 |
query_peripheral_data |
source、point_id、start、end、limit、offset | 固定外围 points、work_orders、observations、test_data 适配器;保留 synthetic |
get_task_evidence |
evidence_id、data_path、limit、offset | 当前任务的 evidence/{id}/snapshot;项目、任务及可读轮次由绑定约束 |
初版为24项查询;新增空间邻近查询后,当前为25项查询,连同10项分析共35项Server工具。旧 CLI 的 data pipeline-health 实际触发模型预测,不能包装成“读取已保存结果”;它作为 predict_pipeline_health 的后续计算候选,先核对模型资产、执行成本和计算权限。旧 CLI 的 9 条 POST 执行命令也仅列作后续候选,不加入查询目录。
空间邻近查询沿用 webgis.view 和宿主绑定项目,数据库连接不从工具参数选择。app/infra/db/postgresql/spatial_queries.py 使用既有 gis.* 模型视图及GiST几何索引做保守候选筛选,再转换到WGS84 geography,按点到完整几何的椭球最短距离以米筛选和排序,最后在数据库中执行LIMIT/OFFSET,不把全网几何交给Agent。跨日期变更线时拆分候选包围框,极区跳过Mercator候选筛选;空或缺失几何不参与查询。距离语义依据 PostGIS ST_DWithin 和 ST_Distance,不把EPSG:3857平面距离直接当作实际米数。
输出 data.source=model_network、center_crs=EPSG:4326、distance_method=WGS84_spheroid,data.items 包含 element_id、distance_m 和 properties;page.has_more 与 truncated 保留分页边界。没有命中返回空items,不自动扩大半径;空间邻近不证明拓扑连接或供水归属,道路/河流交叉位置必须由调用方先确认。查询预算仍为30秒,调查调用沿用现有证据与回执保存,不新增REST查询桥或数据库迁移。
聚焦验证:python -m pytest tests/unit/test_mcp_spatial.py tests/unit/test_mcp_query.py -q;设置 MCP_SPATIAL_TEST_DSN 后运行 python -m pytest tests/integration/test_mcp_spatial_postgis.py -q 可验证实际PostGIS距离与分页,测试只创建事务内TEMP表并回滚,不写业务数据。
权限保持当前真实规则:管网、GIS 和分析运行读取沿用 webgis.view,SCADA 沿用 scada.view,模型基线和分析时序沿用 simulation.view;监测点选址路由当前整体要求 optimization.run,MCP 不擅自改成 optimization.view。历史比较模式额外核对其中使用的模拟数据权限。拆分读权限需要单独的后端授权设计,不能借 MCP 接入顺便降级。
新源 registry 明确 source、所需权限、查询参数 Schema、数据语义、结果上限、任务 scope 和是否允许证据保存。原 7 个 TaskDataSource 与新源按明确规则扩展,历史证据仍可反序列化。既有已批准 scope 只迁移到与其原意等价的查询能力,不用“只读”作为放开所有源的理由。
分页 大结果与数据语义
集合查询默认 limit=20,最大 500;分析运行最大 100。offset 最大10000,超过时缩小时间窗或目标。常规集合在 SQL 中限制返回量。带总量的集合返回 items、total、limit、offset、has_more;没有稳定总量时 total=null,不伪造总数。Agent 首次优先单对象、必要的短时间窗和小页;只有明确问题仍缺证据且 has_more=true 时继续翻页,不自动遍历全网。
历史组合查询按稳定的序列顺序(设备ID、原始/清洗,再按类型和元素ID排列模型序列)及时间对数据点分页;在同一条 UNION ALL SQL 上应用全局 limit/offset,并多读一个点计算 has_more。证据查询按绑定项目、任务和证据ID,在数据库中选取 data_path 和页;嵌套集合返回路径描述,数组保持索引顺序,对象按 C 排序,字符串按 Unicode 字符分页。证据页和完整资源在返回数据库客户端前检查8MiB上限。此限制约束传输和应用进程,不代表数据库无需读取或解压 JSONB。
DEMO 样本仍需校验整份 RFC 8785 摘要,不能仅验证所选页。数据库先检查整份 JSON 文本大小,超过8MiB则拒绝传输;预算内读取完整样本,校验后筛选和分页。超限须由数据源拆分样本,不能通过反复缩小查询窗口消除该限制。
时序默认最多 20 个目标、24 小时,最多返回 10000 条或 8 MiB。全网模型基线接口当前没有元素筛选和结果分页,接入前必须在共用 service 增加有界筛选/游标;MCP 不直接代理当前无限制接口。批量历史采用其现有 mode 和 run_id 语义,不假定新接口完全等价于旧三类 composite 查询。
结果超过内联 12000 字节时返回摘要、数量、时间范围、truncated、继续查询参数或受保护 resource_link。第一版 resources 暴露已有任务证据快照,普通查询用分页继续读取;不新增全局 result-ref 存储或任意主机文件路径 API。资源读取同样核验令牌和上下文,不能凭 URI 得到数据。
共用输出对象包含 source、source_ref、queried_at、origin、data、window、units、page、truncated、data_gaps,以及调查时的 evidence_id。原 service 返回结构由各工具的 Pydantic 输出模型明确描述,不靠自由文本或一个无约束的 data 字段作为最终契约。第一版每主体/项目最多 4 个并发查询;超过并发返回可重试限流结果,队列不无限增长。查询执行预算沿用 Server 已配置的访问超时,认证续期等待单独计时。
普通原始查询错误返回结构化业务错误,不改成空数据。结果标明数据来源、查询时间、源时间窗口、单位、SCADA 实测或清洗、模型基线或分析运行、synthetic、缺失及截断。truncated=true 不能支持“完整范围未见异常”的结论。
调查证据与幂等
共享查询入口根据已验证 purpose 选择包装方式。普通聊天返回查询结果;调查及 reader 查询在返回前调用现有 OperationalTaskService.record_data_snapshot,保存范围与来源,再返回 snapshot 和 evidence_id。外部 MCP 返回内容不能通过这一入口被重新标成内部系统证据。
原生 callID 经宿主转为 application request key。Server 按主体、项目、会话、任务、run、callID 和规范参数记录查询回执;同 key、同参数重试读取已保存 snapshot,同 key、不同参数拒绝。MCP 断线重发使用新 JSON-RPC ID,但保持同一 application request key;证据存储提交失败就返回失败,不能先返回成功再异步补证据。
调查取数前后都校验本轮仍然有效。长查询期间不持有任务行锁;完成后用短事务确认轮次并保存证据。查询途中被新轮次替代时,不把晚到结果写进新轮次,也不复活旧运行。外部网络请求与数据库读写使用短事务,不跨网络等待持锁。
查询业务源是只读,但调查包装会追加证据,因此相应工具不能仅根据 GET 方法声明 readOnlyHint=true。固定目录内可能保存证据的工具保守标为 readOnlyHint=false、destructiveHint=false,执行策略按已审查的精确工具名允许“读源并追加系统证据”,不由 annotation 决定授权。精确 grant 不覆盖报告发布、工单或设备动作。
客户端策略与生命周期
内部业务查询在项目会话、调查执行和已验证 task_reader 中开放对应精确工具。reader 的精确白名单仅增加可信 mcp_tjwater_* 项目查询目录;现有外部 MCP 禁用规则继续执行,没有向模型开放通用 resources 桥或子 Agent 写权限。
取消传播至 OpenCode 原生 MCP、本地 MCP、产品 MCP 客户端、Server handler 和底层工作。接口断线时停止请求,宿主不会无条件后台补跑。读查询只允许一次认证重试和一次可判定未完成的传输重试;调查依 application request key 防止重复保存。
本地 MCP 采用已有内部 token 和浏览器来源拒绝规则;它只证明可信 OpenCode 到宿主的通道,不能替代 Server 的 OAuth 主体认证。日志保留主体、项目、任务、run、工具名、耗时、数量、取消与错误类别,不保存 Bearer、secret、context_handle 或原始敏感数据。
MCP 服务失败时其调用返回可重试错误,现有 REST 和原生查询仍可使用;如果管理员启用 MCP 作为必需产品能力,Agent 启动预热必须验证 server/discover、实际 tools/list、Schema、身份路径和取消,不调用付费推理,不用“连接成功”代替能力就绪。
实施范围与验收
这是两仓库的中等规模改动,预计超过 8 个文件;第一版挂载于已有 Server,不新增独立部署服务。前端正常情况下继续复用现有凭据刷新事件,只有验收发现契约缺口时才扩展。
| 仓库 | 实施落点 |
|---|---|
| Server | app/main.py 挂载和 lifespan;app/core/config.py;新增 app/mcp 的协议、工具与认证适配模块 |
| Server | app/services 的共用查询与上下文绑定服务;app/domain/schemas 的查询/输出契约;任务源与证据回执的持久存储要求 |
| Server | 现有 API service 提取、有界查询;独立 MCP 令牌验证;requirements.txt;.env.example;tests/api、tests/auth、tests/unit;OpenAPI 导出 |
| Agent | src/product 的 MCP 客户端与令牌缓存;现有 credential-refresh;新增内部 MCP call 桥 |
| Agent | .opencode/plugins/tjwater-tools.ts;MCP 和 task-reader 策略;工具 schema 投影;真实目录启动检查;package.json 与锁文件;聚焦测试 |
升级 Server 的官方 mcp SDK 到已发布 v2,按官方迁移文档核对 lifespan、认证、协议模式和结果类型后固定实际发布版本。不能仅升级 fastmcp 并假定新协议成立;已有依赖是否删除,须先确认真实使用与兼容关系。当前 Python SDK 文档示例同时包含 legacy 配置,协议行为以 2026-07-28 规范和实际验收为准。Python SDK v2 迁移说明
部署配置包含固定 Keycloak issuer、规范 MCP resource URI、专用 audience、Agent confidential client ID 与 secret。secret 只在 Agent 宿主环境或密钥管理中,Keycloak 签名密钥由 JWKS 获取并校验固定 issuer,不发给 OpenCode。沿用 worker 自有 client_credentials 配置,不把 worker secret 转交给 Agent。
Server 需提供上下文绑定、撤销和调查查询回执的持久存储;多进程读写须一致,不能用单进程 dict 宣称重启或横向扩容可用。应用层只保存令牌派生身份和范围,不保存 token。存储由后端按现有数据库管理流程交付,本设计不执行数据库迁移。
验收必须覆盖:
- 实际
server/discover、24 工具目录、版本固定、标准_meta、resultType、缓存提示;头与正文不一致、未知工具、非法 schema、旧握手的错误行为。 - 两个用户两个项目并发,交错调用也不串凭据;句柄换用户、项目、client、task、run 或失效均拒绝;个人会话不得获得业务数据。
- 普通登录无需再次登录 MCP;401续期一次、403不升权;并发 refresh 合并;注销、成员撤销和背景服务凭据续期。
- worker 无浏览器完成查询;reader 只取父任务范围;不获得报告、审批、控制、Shell 或其他外部 MCP。
- 管网属性与分页、SCADA 原值/清洗值、模型基线、分析 run、历史三种 mode、GIS 与外围 synthetic;不支持的旧功能返回明确能力缺口。
- 24小时/目标数/分页边界、实际服务端限量、资源读取授权;超大结果不会全量进入内存或聊天。
- 查询成功且证据已经持久化;断线重试仅一个证据回执;查询中轮次被替代、取消、DB失败和认证服务失败均不误报成功。
- REST 与 MCP 的权限和数据结果相同;新 SDK 不改变原 API、worker 或报告读取;启动真实目录检查不调用模型推理。
使用 Server 现有 conda 环境运行新增 MCP 聚焦 tests/api、tests/auth、tests/unit;Agent 执行 bun run check 与新增凭据、MCP桥、策略、目录聚焦测试。用户登录、refresh 和两项目隔离需用现有前端做一次实际浏览器验收。设计文档本身仅进行路径检查与 git diff --check,不执行业务写请求。
交付可以分两次独立上线:先增加服务端只读 MCP、OAuth与上下文,保留现有产品调用;再接入 Agent 宿主客户端和 reader,验证后切换产品查询。任何一步失败都可以回到已有原生工具,不能在单次失败时自动切换凭据绕过权限。回滚停用 MCP 入口/宿主注册并撤销上下文,保留已经保存的调查证据与旧报告。提交、身份平台变更、数据库供给和部署另按实际实施授权执行。
旧 CLI 与当前后端对照
路径均为 /api/v1 下的当前能力;“执行候选”不加入第一版查询 MCP。此表依据当前源码生成 OpenAPI 和旧 CLI handler,而非线上算法成功证明。
| 旧 CLI 命令 | 当前路径或处理 | 第一版决定 |
|---|---|---|
| network get-junction-properties | GET /junctions/properties | get_element_properties |
| network get-pipe-properties | GET /pipes/properties | get_element_properties |
| network get-all-pipes-properties | GET /pipes | list_elements 分页 |
| network get-reservoir-properties | GET /reservoirs/properties | get_element_properties |
| network get-all-reservoirs-properties | GET /reservoirs | list_elements 分页 |
| network get-tank-properties | GET /tanks/properties | get_element_properties |
| network get-all-tanks-properties | GET /tanks | list_elements 分页 |
| network get-pump-properties | GET /pumps/properties | get_element_properties |
| network get-all-pumps-properties | GET /pumps | list_elements 分页 |
| network get-valve-properties | GET /valves/properties | get_element_properties |
| network get-all-valves-properties | GET /valves | list_elements 分页 |
| component option schema | GET /network-schemas/{kind} | get_network_schema |
| component option get | GET /network-options 及 time、energy、pump-energy | get_network_options |
| simulation run | POST /simulation-runs | 执行候选,duration 为分钟 |
| analysis burst | POST /burst-analyses | 执行候选 |
| analysis valve isolation | POST /valve-isolation-analyses | 执行候选 |
| analysis flushing | POST /flushing-analyses | 执行候选 |
| analysis age | /water-age-analyses 未注册 | 不开放,不假定算法仍可运行 |
| analysis contaminant | POST /contaminant-simulations | 执行候选 |
| analysis sensor-placement run | POST /sensor-placement-runs | 执行候选 |
| analysis sensor-placement list | GET /sensor-placement-runs | list_sensor_placement_runs,有界适配 |
| analysis sensor-placement get | GET /sensor-placement-runs/{run_id} | get_sensor_placement_run,读取项目路由中的既有成果 |
| analysis runs list | GET /analysis/runs | list_analysis_runs |
| analysis runs get | GET /analysis/runs/{run_id} | get_analysis_run |
| analysis runs results | GET /analysis/runs/{run_id}/results | list_analysis_results |
| analysis leakage identify | POST /leakage-identifications | 执行候选 |
| analysis burst-detection detect | POST /burst-detections | 执行候选 |
| analysis burst-location locate | POST /burst-locations | 执行候选 |
| data timeseries realtime links | GET /timeseries/realtime/links | query_model_baseline_series,已补最多20个元素筛选及SQL限量 |
| data timeseries realtime nodes | GET /timeseries/realtime/nodes | query_model_baseline_series,已补最多20个元素筛选及SQL限量 |
| data timeseries realtime simulation-by-id-time | GET /timeseries/realtime/simulation-results | get_model_baseline_at_time |
| data timeseries realtime simulation-by-time-property | GET /timeseries/realtime/records | query_model_baseline_field |
| data timeseries analysis link-field | GET /timeseries/analysis/runs/{run_id}/links/{link_id} | query_analysis_series |
| data timeseries analysis node-field | GET /timeseries/analysis/runs/{run_id}/nodes/{node_id} | query_analysis_series |
| data timeseries analysis values | GET /timeseries/analysis/runs/{run_id}/values | query_analysis_values |
| data timeseries scada query | GET /timeseries/scada-readings 及 fields | query_scada_readings |
| data timeseries composite | 原三个 views 路径未注册;现有 POST /timeseries/views/element-history/query | 按三种 history mode 重新适配,不直接宣称等价 |
| data pipeline-health | GET /pipeline-health-predictions,实际进行预测 | 后续计算候选,先验证模型资产 |
| data scada get | 原 /scada-devices/detail 未注册;现有 GET /scada-devices/{device_id} | 修正为 get_scada_device |
| data scada list | GET /scada-devices | list_scada_devices 分页 |
| data scada schema | GET /network-schemas/scada-device | get_network_schema |
当前 Agent 的 ProjectDataClient 读取单个元素等能力仍较窄;其中 SCADA 设备读取仅列表,缺少单设备新路径入口。MCP 切换前以本表和当前 Schema 对齐,不能从旧 CLI 直接复制参数或依赖其全量分页行为。
任务上下文查询支持可选的正整数 report_revision;未显式指定时宿主保留协作会话绑定的报告修订号,避免将历史报告替换为最新报告。