Files
next-tjwater-server/docs/mcp-query-implementation.md
jiang 1d061f6099
Generic Container CI/CD / test-build-publish (push) Failing after 6s
Server CI/CD v2 / build-test-publish-and-deploy (push) Failing after 6s
feat: add project-scoped operations
2026-10-22 18:40:20 +08:00

109 lines
16 KiB
Markdown
Raw Permalink 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 查询:本地实施与启用
实现使用 MCP 2026-07-28、Python mcp 2.3.0、TypeScript @modelcontextprotocol/client 与 @modelcontextprotocol/server 2.3.1。Server 提供 `/mcp` 和67项工具(42项查询、12项计算、13项预览式写入);Agent 将其投影合并到本地 MCP,由 OpenCode 原生发现为 `mcp_tjwater_*`,普通项目会话、调查主 Agent 和已验证的直接只读子 Agent 共享42项查询;12项计算仅向非reader会话开放,13项写入仅向项目成员的普通聊天开放。个人会话不能获取项目凭据。旧 CLI 只作为能力清单,没有运行时 CLI 桥接。
`run_simulation` 是在线重算,复用 REST 在线计算服务而不复制数据库;按水力步长更新模型条件和实时基线,不创建正式方案。MCP 时长为秒,原 REST 时长仍为分钟。其 annotation 为 `readOnlyHint=false`、`destructiveHint=true`;只读 reader 不能调用,其他方案模拟继续使用隔离临时模型。详细边界见 [MCP 分析工具说明](mcp-analysis-tools.md)。
计算工具支持Agent按次指定 `timeout_seconds`,默认120秒;计算上限默认1小时,由 `TJWATER_MCP_MAX_CALCULATION_TIMEOUT_SECONDS` 配置并在工具schema公布。Agent、OpenCode和本地HTTP调用预算随目录同步,普通查询仍保持原有短超时。等待超时不意味着底层线程已停止或在线基线已回滚,禁止自动重试整个写入窗口。
## 认证与数据边界
Agent 根据401 challenge 或路径/根 well-known 发现受保护资源元数据,只选择配置中预注册的固定 issuer,再按规范顺序发现 OAuth/OIDC 元数据并严格核对 issuer。token endpoint 从已校验元数据读取,支持 client_secret_post 和 client_secret_basic。公开发现不带产品 Bearer 或 secret,文档最多64KiB、总预算10秒,拒绝重定向和远程明文地址;元数据缓存最多5分钟,MCP 401 后重新发现。产品 Bearer 只发给该 issuer 声明的 token endpoint,采用 Standard Token Exchange V2 取得 MCP 专用短期 Bearer,检查主体不变、issuer、audience、azp。Server 独立用缓存 JWKS 核验签名、issuer、audience、scope 和最长15分钟寿命。普通 REST 额外拒绝 MCP audience 或宿主 azp 的令牌,防止 audience 配置重叠时混用两类令牌。
上下文由专用宿主申请:绑定主体、client、项目、原生会话、用途、工具目录、权限上限和已批准的七类任务数据 scope;调查另绑定 task/run/execution_request/父会话。每次查询重新核验成员和权限;调查必须仍由父会话持有有效租约。资源读取仅限句柄绑定的任务。模型输入没有项目、network、URL、header、Bearer 或 context_handle。
调查数据返回前,在短事务内追加现有任务证据并保存调用回执。同一主体、会话、任务、run 和原生 call ID 的重试复用证据;不同参数或失效范围明确报错。七类原有访问申请 scope 保持原契约,MCP 的新增来源只扩充快照来源枚举。没有新增报告发布、审批、控制、Shell 或递归委派权限。
普通查询默认20条;目录类最多500条,时序最多10000条、20个标识、24小时;offset 最大10000。Agent 首次优先单对象、必要的短时间窗和小页,仅在证据不足时按需扩大,不自动遍历全网。SQL 有 limit/offset 和 statement_timeout。历史对比在同一条SQL中按序列、时间对数据点分页,返回 page.has_more;非零offset也标明覆盖不完整。历史曲线展示调用初始每页500点,`RESULT_TOO_LARGE` 时原位减半重试,最多读取1000条、发出16次请求,共用120秒超时(单次普通查询仍为30秒),图表最多100个有效点;这不改变模型首次查询默认值或时序接口上限。
单份结果超过8MiB报错;超过12KB的调查工具消息只返回证据资源引用,完整快照仍保存,Server 的 `resources/read` 可以读取它。Agent 本地 MCP 只提供工具,将上游资源引用转为 `evidence_read` 中的 `mcp_tjwater_get_task_evidence` 参数,不向 OpenCode 泄露不可在本地读取的链接。`get_task_evidence` 在数据库中按 data_path、limit、offset 选页,嵌套集合和超过256字符的字符串返回路径描述;字符串按 Unicode 字符分页。页和完整资源均在数据库返回前检查8MiB上限,保留原证据编号和合成来源;普通查询要求缩小范围。其他结果仍保留执行后的整体8MiB检查,不能把它理解为所有查询均有数据库端字节限制。原始/清洗实测、模型基线、分析结果、比较和 synthetic 均明确标记。非有限数值转为 null 并记录数据缺口。
`get_task_context` 默认 section=summary,只查固定元数据、160字符任务标题及200字符结论预览,并返回 available_sections;preview_only 和 truncated 明确说明不是完整报告。按 section=task/report/evidence/actions/progress/access_requests/plan/run 读取详情,report_revision 保留选择旧报告的语义。data_path 选择嵌套值,limit 默认20、最多500,offset 最大10000;证据、动作和授权申请集合以稳定 ID 为键,按 ID 字典序分页。字符串按字符分页。详情在数据库返回前检查8KB预算,超限缩页或选更深路径。缺少可选进度或授权申请表时返回 metadata.available=false,不冒充空事实。绑定权限范围只读取七类既有 scope,不再加载整个任务上下文。不增加工具数量、数据库表或迁移,也不改变原 REST 完整上下文契约。
外围 MCP 直接读取所选项目 BizDB 的样板事实和既有时序。它不执行外围样板 HTTP 的故障场景注入;混有合成记录的结果按 synthetic 保守标记。DEMO-* 观测在数据库端拒绝超过8MiB的完整样本,预算内仍需读全样本、核验摘要后筛选并保留 synthetic;此限制不能通过缩小查询窗口解决。监测点列表沿用设备目录,不把未登记的合成点冒充实测设备。
## 启用步骤
### Server 分发业务处置 Skill
业务流程源文件位于 `resources/work-order-skills/*/SKILL.md`,随 Server 镜像分发。`get_task_context` 摘要提供 `handling_skill`(适用 workflow、名称、版本和 SHA-256)及 `display_time_zone=Asia/Shanghai`;使用 `section="skill"` 读取完整正文。流程由服务端持久化任务的 workflow 选择,调用方不能传入文件路径或其他任务类型;未知 workflow 返回 `available=false`,不自动套用其他流程。
当前覆盖爆管上报、水质投诉与预约跟进。业务 Skill 描述风险核查、隔离停水、用户通知、现场处置和恢复条件;Agent 本地 Skill 维护通用执行方法。权限、审批和执行仍由 Server 代码控制。流程说明没有 evidence_id,不记录成事件证据。具体草稿由现有 `propose_task_action` 保存并等待人工审批;人工通知等步骤通过关联预约工单承接,不能当作通知已发送或现场工作已完成。
接口复用现有任务绑定、工具权限和授权上下文,不新增 MCP 工具、REST API 或数据库迁移。更新后需让 Server 加载新代码,并重启 Agent 重新获取工具 schema 和指令;不自动重跑既有调查或改写已发布报告。后续单独修改流程正文由 Server 从打包资源读取,版本随正文一起维护,SHA-256 可区分同版本内容变化。
### 2026-10-15 实际运行环境
当前 `conda server` 已升级至 MCP 2.3.0、Pydantic 2.14.1、AnyIO 4.15.1,移除未使用且与 SDK 2 冲突的旧 fastmcp,并将 httpcore 更新至 requirements 中的 1.0.9;`pip check` 通过。旧环境保留为 `server-before-mcp-20261015`,配置、项目库和原生会话数据库的备份保存在私有 `data/mcp-rollout/`。
五个启用项目的业务库均已完成完整备份、归档目录可读检查、025 迁移回滚演练和正式应用。新项目仍须通过项目数据库迁移流程准备 MCP 表,不会在查询请求中自动创建。
后端通过 [tjwater-server.service](../infra/systemd/tjwater-server.service) 运行于 127.0.0.1:8000;Agent 运行于 8787,保留原生会话数据库和用户会话文件。两端使用 `https://keycloak.waternetwork.cn/realms/tjwater`,请求方为 `agent-mcp`,目标 audience 为 `tjwater-mcp`。worker 取令牌地址和前端登录 issuer 也已统一为该域名,避免 HTTP issuer 签发的令牌在 HTTPS 端点交换失败。
真实 Keycloak 及项目验证已覆盖 24 项后端目录、30 项 Agent 原生目录、管网分页、SCADA 设备与读数、分析运行和模型时间选项。读数请求成功但当前验证窗口为空,不代表已有完整实测数据。正式就绪与目录检查不调用模型推理。
以下为新环境的接入步骤。
本地 HTTP 身份服务的显式开发白名单见 [Keycloak 地址配置](mcp-keycloak-setup.md#4-地址数据库和启用顺序)。正式配置使用 HTTPS 域名;开发例外只适用于指定内网 issuer,不放行远程 HTTP MCP resource。
Keycloak 管理员可先按 [MCP 认证客户端配置清单](mcp-keycloak-setup.md) 操作。已有请求方客户端 `agent-mcp` 时直接使用该 ID,并同步 Agent 的 `TJWATER_MCP_CLIENT_ID` 与 Server 的 `TJWATER_MCP_HOST_CLIENT_ID`;下方默认示例中的 `tjwater-agent-mcp` 应相应替换。
1. 为 Server 安装更新后的 requirements。SDK 2 需要新的 Pydantic/AnyIO;升级现有环境前保留完整回退环境,移除没有业务引用且与 SDK 2 冲突的旧 fastmcp 依赖,并执行 `pip check`。不要依赖旧环境生成新版 OpenAPI。
2. 按项目 BizDB 迁移流程执行 `resources/sql/025_mcp_query_contexts.sql`。先备份并检查 workbench schema 已存在;脚本只新增上下文/回执表和索引。每次建立句柄最多清理100个本项目已失效句柄;证据/回执保留遵循任务归档策略。
3. 配置 Keycloak:专用 confidential requester `tjwater-agent-mcp`,启用 Standard Token Exchange V2,设置目标 audience `tjwater-mcp`,配置 `mcp.catalog.read`、`mcp.query`、`mcp.context.bind`。产品前端和 worker 的源 token 需要包含 requester audience;源 token 不应包含目标 MCP audience。交换令牌须保持源主体,azp 为 requester,RS256,寿命至多900秒,建议300秒。目录 client_credentials 只申请 catalog scope,也须带目标 audience;目录服务身份不因此获得项目业务权限。
4. 在 Server 受保护环境设置:
```dotenv
TJWATER_MCP_ENABLED=true
TJWATER_MCP_ISSUER_URI=https://identity.example/realms/water
TJWATER_MCP_RESOURCE_URI=https://server.example/mcp
TJWATER_MCP_AUDIENCE=tjwater-mcp
TJWATER_MCP_HOST_CLIENT_ID=tjwater-agent-mcp
TJWATER_MCP_ALLOWED_HOSTS=["server.example","localhost:*","127.0.0.1:*"]
TJWATER_MCP_ALLOWED_ORIGINS=[]
```
5. 在 Agent 宿主受保护环境设置:
```dotenv
TJWATER_MCP_ENABLED=true
TJWATER_MCP_ISSUER_URI=https://identity.example/realms/water
TJWATER_MCP_RESOURCE_URI=https://server.example/mcp
TJWATER_MCP_AUDIENCE=tjwater-mcp
TJWATER_MCP_CLIENT_ID=tjwater-agent-mcp
TJWATER_MCP_CLIENT_SECRET=<由身份平台注入>
```
安装 Agent 更新的 Bun 依赖。本地 MCP 使用官方 Server SDK,独立监听127.0.0.1临时端口,先于原生工具发现就绪。宿主只把公开端点写入 `SESSION_METADATA_STORAGE_DIR` 的兄弟目录 `mcp-query/connection.json`,目录0700、文件0600;不保存令牌或 client secret。embedded 自动注入连接路径,OpenCode 环境仍剔除全部 `TJWATER_MCP_*`。client 模式须同网络空间、同版本可信插件、同一内部密钥,并共享 `TJWATER_AGENT_MCP_CONNECTION_PATH` 指向的连接文件,Agent 重启后同步重载原生连接。默认路径 `data/mcp-query/connection.json`。不要把 OAuth 配置注入外部 OpenCode。
项目及宿主只读工具统一由 MCP 提供,插件只配置原生连接与权限;额外六项是地理编码、会话搜索、调查任务列表与详情、消息读取、待办读取,合并后共73项。公开网页搜索使用 OpenCode 原生 `websearch`,搜索服务及凭据由 OpenCode 管理;Server 的 Bocha 接口、配置和 MCP 搜索入口已移除。`memory_manager` 和 `skill_manager` 保留完整读写插件,报告/消息/现场业务动作与展示工具保持插件形式;新增模型和数据维护写入通过预览式MCP提供。旧项目和任务查询工具及其内部 HTTP 查询桥已移除;未启用产品 MCP 时项目数据不可用,不回退。
原生调用的 `_meta["ai.opencode/sessionID"]` 仅用于关联宿主会话,本地服务先核验内部密钥和本机来源,再核验所有权及直接父子关系。原生工具发现、权限、元数据传递与待办 RPC 已由 `bun run tests/integration/query-mcp-smoke.ts` 验证,不调用模型。任务上下文保留可选 `report_revision`,缺省使用协作会话绑定修订号。隔离启动器显式设置自己的 `TJWATER_AGENT_MCP_CONNECTION_PATH`,两端共同读取该文件,不继承其他实例的连接文件。
6. 先用测试项目分别验证普通项目会话、worker 调查和直接 reader 子会话,再开启生产。Agent 启动先发现 Server 目录并与宿主67项白名单对齐,再验证 OpenCode 的真实工具 ID;失败不进入 ready。切换开关需重启 Agent。Server 未迁移项目的绑定接口返回503。
client_credentials 与 token exchange 均发送标准 `resource=TJWATER_MCP_RESOURCE_URI`;exchange 同时保留配置的 audience。部署前必须验证身份平台接受该参数,且两种 grant 都将规范资源映射到 Server 要求的专用 audience。若所用 Keycloak 拒绝该参数,会报告配置不兼容,需升级或提供兼容适配;代码不会移除 resource 后重试。目录的服务 grant 与用户/worker 的 exchange 均不会退回共享管理员 Bearer或直接转发产品 Bearer给MCP。[Keycloak 官方说明](https://www.keycloak.org/securing-apps/token-exchange)。外部交互客户端的授权码流程和第三方客户端绑定不包含在本次交付中。
HTTP 版本校验先核对头体一致性,再报告不支持的版本:不一致返回400/-32020;双方一致且版本不支持才返回400/-32022及支持列表。
## 验证
Server:
```bash
python -m pytest tests/unit/test_mcp_query.py tests/unit/test_timeseries_history.py tests/unit/test_timeseries_history_schema.py tests/api/test_workbench_runtime_endpoints.py tests/api/test_history_query_permissions.py tests/unit/test_operational_data.py tests/auth -q
python scripts/check_openapi.py
```
Agent:
```bash
bun run check
bun test tests/product/mcpAuthDiscovery.test.ts tests/product/mcpQueryClient.test.ts tests/product/credentialRefresh.test.ts tests/runtime/task-agent-policy.test.ts tests/runtime/opencodeConfig.test.ts tests/opencode/mcpPolicy.test.ts tests/opencode/productMcp.test.ts
MCP_TEST_PYTHON=<隔离Server环境Python> MCP_TEST_SERVER_ROOT=<Server仓库绝对路径> bun tests/integration/mcp-query-smoke.ts
```
HTTP smoke 只启动自己的 loopback fixture,使用真实两端 SDK,无生产数据库、Keycloak或模型推理。它验证新版发现、真实资源元数据 HTTP 读取、授权服务器元数据及带 resource 的两种 grant(后两者为桩)、67项工具、主机绑定、查询及撤销;真实环境验收记录见上方实际运行环境一节,不能用 fixture 结果代替。前端严格解析已接受这些 MCP 快照来源,未加入浏览器 MCP 凭据或新 UI。
查询限制回归使用 `python -m pytest tests/unit/test_mcp_query_limits.py tests/unit/test_mcp_query.py tests/unit/test_timeseries_history.py tests/unit/test_timeseries_history_schema.py -q`。实际 PostgreSQL SQL 验证可设置 `MCP_QUERY_TEST_DSN` 后运行 `python -m pytest tests/integration/test_mcp_query_limits_postgres.py -q`;该 DSN 必须指向空的、可丢弃的独立数据库,测试创建临时业务同名 schema 并在事务结束时回滚。它验证数据点跨序列翻页、证据路径/Unicode分页、任务摘要与指定报告、章节稳定路径、项目/任务隔离、既有scope边界、字节门槛和合成摘要,不代替真实项目验收。