説明なし

Guangyu 522b113edf improve topo tools description 3 週間 前
instrument_config_mcp 522b113edf improve topo tools description 3 週間 前
scripts 38d100611f feat: 添加仪表点位查询功能 3 週間 前
.dockerignore 895c5aaa00 init 3 週間 前
.gitignore 895c5aaa00 init 3 週間 前
Dockerfile ad9a2bbc42 refactor(Dockerfile): 移除前端构建检查步骤 3 週間 前
README.md 522b113edf improve topo tools description 3 週間 前
mcp-design.md 895c5aaa00 init 3 週間 前
pyproject.toml 298a137cf2 build: 添加 psycopg2-binary 依赖 3 週間 前
uv.lock ce4209cb95 build: 添加 psycopg2-binary 依赖 3 週間 前

README.md

Instrument Config MCP

基于 FastMCP 的只读配置查询 MCP 服务,用于访问以下接口:

  • 位置管理
  • 系统管理
  • 设备类型
  • 仪表类型
  • 设备搜索
  • 仪表搜索

服务特点:

  • 通过 project_key 选择项目环境
  • 自动处理项目登录和 token 缓存
  • 正式查询参数以 id 为准
  • 分页保持后端原样,不自动翻页
  • 返回结果尽量保持后端原始结构

环境要求

  • Python 3.11 到 3.13
  • 一个可访问的 sys_config
  • DATABASE_URL 指向包含 sys_config 的数据库

Windows 下建议固定使用 uv 的 Python 3.13 环境,避免 pywin32 在 Python 3.14 下的兼容问题。

默认数据库地址:

sqlite:///llm_proxy.db

默认上游超时:

UPSTREAM_REQUEST_TIMEOUT=60

项目配置

服务会从 sys_config 中读取 key 为 mcp_project_data_projects 的 JSON 数组。

示例:

[
  {
    "project_key": "dev-01",
    "project_name": "DEV开发环境",
    "base_url": "http://192.168.1.109:32080",
    "username": "admin",
    "password": "123456",
    "enabled": true
  }
]

安装

在项目根目录执行:

uv sync --python 3.13

启动

默认以 HTTP 模式启动,默认地址:http://127.0.0.1:8500/mcp

方式一:使用模块启动

uv run --python 3.13 python -m instrument_config_mcp

方式二:使用 console script 启动

uv run --python 3.13 instrument-config-mcp

可选环境变量:

  • MCP_HOST,默认 127.0.0.1
  • MCP_PORT,默认 8500
  • MCP_PATH,默认 /mcp

如果需要指定数据库:

set DATABASE_URL=sqlite:///llm_proxy.db
uv run --python 3.13 python -m instrument_config_mcp

如果需要指定 HTTP 地址:

set MCP_HOST=0.0.0.0
set MCP_PORT=8500
set MCP_PATH=/mcp
uv run --python 3.13 python -m instrument_config_mcp

如果需要指定上游超时:

set UPSTREAM_REQUEST_TIMEOUT=60
uv run --python 3.13 python -m instrument_config_mcp

MCP 工具

当前提供以下工具:

  • project.list
  • list_locations
  • list_system_tree
  • list_systems
  • list_device_types
  • list_meter_types
  • search_devices
  • search_meters
  • search_points
  • topology_group_list
  • topology_list
  • topology_get_node
  • topology_find_context

project.list 外,其他工具都必须传 project_key

拓扑能力概览

当前拓扑能力采用“上游拉取 + 本地缓存索引 + 缓存查询工具”的模式。

目标:

  • 通过本地缓存索引替代每次实时全量扫描拓扑
  • 支持按设备/仪表快速定位所在拓扑及节点上下文
  • 为异常分析场景提供稳定、可重复的查询能力

当前实现主要落在以下文件中:

  • instrument_config_mcp/config_api.py
  • instrument_config_mcp/topology_cache.py
  • instrument_config_mcp/server.py

当前已落地的核心能力:

  • 从上游读取拓扑分组列表
  • 从上游读取单个拓扑结构
  • 支持树形拓扑和图形拓扑两种解析路径
  • 缓存拓扑分组、拓扑主信息、节点、边、实体索引
  • 基于缓存查询拓扑列表
  • 基于缓存查询节点局部上下文
  • 基于缓存按设备/仪表定位拓扑上下文

拓扑上游接口

当前代码实际使用的上游接口只有两个,均定义在 instrument_config_mcp/config_api.py

Python function Upstream API Purpose
list_topologies_with_group(project_key, group_ids=None) POST /api/configapi/topo/list_with_group 拉取拓扑分组与拓扑列表
get_topology(project_key, id) POST /api/configapi/topo/get 拉取单张拓扑详情

说明:

  • 当前没有实现 topo/get_data 的封装与使用
  • 当前缓存设计围绕“结构查询”和“异常定位”场景,没有引入展示层时序数据缓存

拓扑缓存刷新

核心入口是 refresh_topology_cache(project_key, topology_ids=None)

实际行为:

  1. 校验 project_key
  2. list_topologies_with_group(project_key, group_ids=[]) 拉取全量拓扑树
  3. 从上游返回中提取分组记录和拓扑候选记录
  4. 如果传了 topology_ids,只刷新指定拓扑;否则刷新该项目全部拓扑
  5. 对每张目标拓扑调用 get_topology(project_key, topology_id)
  6. 根据 data.diagram 形态选择解析策略:list 走树形拓扑,dict 走图形拓扑
  7. 解析后写入本地缓存表

刷新结果会返回以下统计字段:

  • refreshed_group_count
  • refreshed_topology_count
  • refreshed_node_count
  • refreshed_edge_count
  • refreshed_entity_index_count
  • topology_ids
  • refreshed_at

刷新模式:

Mode Trigger Behavior
Full refresh topology_ids is None 删除该项目全部拓扑缓存,再重建全部分组、拓扑、节点、边、实体索引
Partial refresh topology_ids 非空 仅删除指定拓扑对应的节点、边、实体索引、拓扑注册信息,但会重建全量分组表

说明:

  • 分组表在局部刷新时也会整体重写,避免分组树和拓扑注册信息不一致
  • 当前没有增量 merge 逻辑,采用“删后重建”的方式,简单且可预测

拓扑解析设计

树形拓扑

入口函数:_parse_tree_topology(project_key, topology_id, diagram)

处理逻辑:

  • 递归遍历树节点
  • 写入 TopologyNode
  • 根据父子关系写入 TopologyEdge
  • 从每个节点的 meter_list / device_list 中抽取实体引用
  • 仅保留“最深层命中的节点”作为实体索引

图形拓扑

入口函数:_parse_graph_topology(project_key, topology_id, diagram)

处理逻辑:

  • 读取 diagram.nodesdiagram.edges
  • _build_graph_node_context(...) 推导节点层级、父节点关系、路径文本、子节点数量
  • 写入 TopologyNode
  • 写入 TopologyEdge
  • 从节点的 meter_list / device_list 中抽取实体引用
  • 同样只保留“最深层命中的节点”进入实体索引

实体索引规则

当前实现采用统一实体索引表:

  • entity_type = meter | device
  • entity_id
  • topology_id
  • node_id
  • depth

其中 depth 表示该实体命中的节点深度。

当前通过 _record_deepest_entities(...) 保证:

  • 同一个实体如果在多个节点出现,优先保留更深层节点
  • 如果同深度有多个节点,则保留多个 node_id

拓扑缓存表结构

当前数据库模型定义在 instrument_config_mcp/topology_cache.py 中。

topology_group

保存拓扑分组树。

关键字段:

  • project_key
  • group_id
  • group_name
  • parent_group_id
  • group_path_text
  • level
  • sort_index
  • refreshed_at
  • is_active

关键约束和索引:

  • uq_topology_group_project_group
  • ix_topology_group_project_parent

topology_registry

保存拓扑主信息。

关键字段:

  • project_key
  • topology_id
  • topology_name
  • topology_type
  • object_type_code
  • group_id
  • root_shape
  • source_updated_time
  • refreshed_at
  • is_active

关键约束和索引:

  • uq_topology_registry_project_topology
  • ix_topology_registry_project_group

topology_node

保存拓扑节点。

关键字段:

  • project_key
  • topology_id
  • node_id
  • node_name
  • parent_node_id
  • level
  • node_type_code
  • refer_id
  • refer_level
  • is_virtual
  • path_text
  • child_count
  • sort_index

关键约束和索引:

  • uq_topology_node_project_topology_node
  • ix_topology_node_project_topology_parent
  • ix_topology_node_project_topology_refer

topology_edge

保存节点边关系。

关键字段:

  • project_key
  • topology_id
  • source_node_id
  • target_node_id
  • sort_index

关键约束和索引:

  • uq_topology_edge_project_topology_nodes
  • ix_topology_edge_project_topology_source
  • ix_topology_edge_project_topology_target

topology_entity_index

保存设备/仪表到节点的反查索引。

关键字段:

  • project_key
  • entity_type
  • entity_id
  • topology_id
  • node_id
  • depth

关键约束和索引:

  • uq_topology_entity_index_project_entity_topology_node
  • ix_topology_entity_index_project_entity
  • ix_topology_entity_index_project_topology_node

Schema 说明:

  • topology_entity_index 同时承载 meterdevice 两类实体关系
  • 刷新时间保存在分组表和拓扑注册表中
  • 缓存刷新采用整批删除后重建的方式,不单独维护刷新状态表

拓扑工具说明

topology_group_list(project_key)

用途:

  • 返回缓存中的拓扑分组树

输出结构:

  • project_key
  • groups
  • total

topology_list(project_key, group_id=None, object_type_code=None)

用途:

  • 返回缓存中的拓扑列表
  • 可按分组或对象类型过滤

输出结构:

  • project_key
  • topologies
  • total

topologies[] 当前包含:

  • topology_id
  • topology_name
  • topology_type
  • object_type_code
  • group_id
  • group_path_text
  • root_shape
  • refreshed_at

topology_get_node(project_key, topology_id, node_id='root', include_siblings=True, include_children=True)

用途:

  • 获取一个节点及其直接邻域
  • 如果 node_id='root',会自动解析该拓扑的根节点

输入约束:

  • node_id 必须是该拓扑缓存中的真实 node_id
  • node_id 不是节点名称
  • node_id='root' 是便捷模式,不要求缓存里真的存在名为 root 的节点
  • 如果不知道真实 node_id,可先用 topology.find_context 从命中结果里的 self.node_id 获取

输出结构:

  • topology
  • node
  • parents
  • children
  • siblings

topology_find_context(project_key, entity_type, entity_id, topology_id=None, include_siblings=True, ancestor_depth=5, descendant_depth=2)

用途:

  • 按实体快速反查命中的拓扑节点上下文

输入约束:

  • entity_type 只允许 meterdevice
  • 不接受中文值,如 仪表设备
  • 不接受复数或其他业务类型,如 metersdevicespointsystem

输出结构:

  • query
  • matches
  • total_matches

每个 matches[] 当前包含:

  • topology
  • self
  • parents
  • children
  • ancestors
  • descendants
  • siblings

拓扑查询行为

分组查询

topology.group_list 直接从 topology_group 组装树结构返回,不再依赖上游实时调用。

拓扑列表查询

topology.list 的过滤逻辑:

  • 如果传 group_id,会把该分组及其全部子分组都纳入可见范围
  • 如果传 object_type_code,再叠加对象类型过滤

节点邻域查询

topology.get_node 返回的是一个节点的直接邻域:

  • 直接父节点
  • 直接子节点
  • 同父兄弟节点

其中:

  • node_id='root' 时,会先自动解析当前拓扑的根节点,再返回该根节点的直接邻域

它不做深层遍历。

实体上下文查询

topology.find_context 会:

  1. 先查 topology_entity_index
  2. 对每个命中结果加载节点图关系
  3. 返回当前命中节点、直接父子、祖先链、后代链、同级兄弟

拓扑存储和初始化

当前拓扑缓存不是单独写入 JSON 文件,而是落在服务使用的数据库中。

数据库连接来源:

  • instrument_config_mcp/db.py 中的 database_url()
  • 默认值是 sqlite:///llm_proxy.db
  • 如果部署时设置了 DATABASE_URL,则以环境变量为准

以默认配置为例:

  • SQLite 文件位于工作目录下的 llm_proxy.db
  • 拓扑缓存表、sys_config 表都在同一个数据库中

当前仓库没有单独提供 .sql 建表脚本,也没有 migration 目录。

建表行为分两类:

  • scripts/init_local_sys_config.py 会执行 Base.metadata.create_all(engine),适合部署初始化时一次性创建 ORM 已注册的表
  • 拓扑缓存相关逻辑会在运行前调用 ensure_topology_cache_tables(),内部同样使用 Base.metadata.create_all(sql_engine())

这意味着:

  • 仅启动 MCP 服务本身,不会做一次全局的统一建表
  • 但首次执行拓扑缓存刷新或查询时,会自动确保拓扑缓存表存在
  • 如果希望部署时显式准备好数据库,建议先运行一次初始化脚本,再启动服务

手动刷新入口

除了 MCP 工具,当前还额外暴露了一个内部路由:

  • GET /topology/cache/refresh?project_key=...

对应函数:refresh_topology_cache_route

能力:

  • 支持通过 query string 触发缓存刷新
  • 支持传多个 topology_id 做局部刷新

说明:

  • 这个路由设置了 include_in_schema=False
  • 它不是正式 MCP 工具,而是给手动测试和运维刷新缓存用的入口

当前限制

从当前代码和设计对照看,还存在这些空缺:

  • 当前没有将拓扑刷新能力以正式 MCP 工具名暴露出去
  • 当前没有 topology.get_structure 这种直接返回整张拓扑缓存结构的工具
  • 当前没有独立刷新状态表,因此无法直接查询某项目最后一次刷新状态
  • 当前也没有对外暴露节点绑定实体列表的专门工具

分页规则

  • 默认 page_size=100
  • 由调用方显式传 page_num
  • 服务不自动翻页
  • 如果当前页后面还有数据,返回中会附带 mcp_note

示例说明:

{
  "state": 0,
  "state_info": "OK",
  "data": {
    "page_size": 100,
    "page_num": 1,
    "total_page": 8,
    "total": 115,
    "data": []
  },
  "mcp_note": "Current result is page 1. If the target was not found, continue with page_num=2."
}

本地联调

仓库内提供了一个简单的 smoke test 脚本:scripts/smoke_test.py

示例 1:查询位置第一页

uv run --python 3.13 python scripts/smoke_test.py list-locations --project-key dev-01 --keyword F1

示例 2:查询系统树

uv run --python 3.13 python scripts/smoke_test.py list-system-tree --project-key dev-01

示例 3:查询设备类型

uv run --python 3.13 python scripts/smoke_test.py list-device-types --project-key dev-01

示例 4:按位置搜索仪表

uv run --python 3.13 python scripts/smoke_test.py search-meters --project-key dev-01 --location-id 162 --show-below true --page-num 1

示例 5:按系统和设备类型搜索设备

uv run --python 3.13 python scripts/smoke_test.py search-devices --project-key dev-01 --system-ids 21 --device-type-ids 5

示例 6:查询某个仪表下的点位

uv run --python 3.13 python scripts/smoke_test.py search-points --project-key dev-01 --id 1785 --page-num 1

联调建议顺序

  1. 先运行 list-device-typeslist-meter-types,确认认证链路正常
  2. 再运行 list-locationslist-system-tree,确认配置接口可访问
  3. 最后运行 search-devicessearch-meterssearch-points 做正式搜索

使用 OpenCode CLI 隔离测试本地 MCP

如果需要另起一个命令行版 opencode 来测试本地 MCP,建议始终使用“隔离数据库 + 隔离用户目录”的方式启动,避免影响当前桌面版 OpenCode 正在使用的会话数据库。

为什么要隔离

如果直接在当前桌面版 OpenCode 环境里再启动 opencode run,可能会遇到以下问题:

  • 复用桌面版注入的环境变量
  • 命中桌面版当前 session 状态
  • Session not found
  • 干扰当前 GUI 会话状态

因此,命令行测试时不要直接复用桌面版进程环境。

前提条件

  1. 本地 MCP 服务已经启动,例如:
uv run --python 3.13 python -m instrument_config_mcp

默认地址:http://127.0.0.1:8500/mcp

  1. 当前机器上已经存在可用的 OpenCode 认证文件:
C:\Users\Guangyu\.local\share\opencode\auth.json

如果 openai-gw 需要鉴权,命令行隔离环境里也要提供这份文件。

推荐测试步骤

下面的步骤会创建一套临时 OpenCode 用户目录,并把 auth.json 复制进去。这样可以保证:

  • 不会写入当前主会话数据库
  • 不会复用桌面版当前 session 状态
  • 仍然可以使用 openai-gw 鉴权

1. 准备隔离目录

$root = 'C:\Users\Guangyu\AppData\Local\Temp\oc-userprofile-openai-gw'
New-Item -ItemType Directory -Force -Path "$root\.local\share\opencode","$root\.cache","$root\AppData\Roaming","$root\AppData\Local" | Out-Null
Copy-Item 'C:\Users\Guangyu\.local\share\opencode\auth.json' "$root\.local\share\opencode\auth.json" -Force

2. 清掉桌面版注入的 OpenCode 环境变量

这一组环境变量如果被继承,opencode run 可能会直接报 Session not found

Remove-Item Env:OPENCODE_SERVER_PASSWORD -ErrorAction SilentlyContinue
Remove-Item Env:OPENCODE_SERVER_USERNAME -ErrorAction SilentlyContinue
Remove-Item Env:OPENCODE_CLIENT -ErrorAction SilentlyContinue
Remove-Item Env:OPENCODE_PID -ErrorAction SilentlyContinue
Remove-Item Env:OPENAI_API_KEY -ErrorAction SilentlyContinue

3. 设置隔离环境变量

$env:USERPROFILE = $root
$env:HOMEDRIVE = 'C:'
$env:HOMEPATH = '\Users\Guangyu\AppData\Local\Temp\oc-userprofile-openai-gw'
$env:HOME = $root
$env:XDG_CONFIG_HOME = 'C:\Users\Guangyu\.config'
$env:XDG_DATA_HOME = "$root\.local\share"
$env:XDG_CACHE_HOME = "$root\.cache"
$env:APPDATA = "$root\AppData\Roaming"
$env:LOCALAPPDATA = "$root\AppData\Local"

4. 确认当前命令会写到隔离数据库

opencode db path

输出应类似:

C:\Users\Guangyu\AppData\Local\Temp\oc-userprofile-openai-gw\.local\share\opencode\opencode.db

如果这里仍然指向默认的 C:\Users\Guangyu\.local\share\opencode\opencode.db,说明隔离没有生效,不要继续测试。

5. 用 openai-gw 跑最小化 MCP 测试

例如先验证 project.list

opencode --pure run --format json --print-logs --log-level INFO --model openai-gw/gpt-5.4 --agent build --dir "C:\Guangyu\Projects\instrument-data-mcp" "调用 instrument-config-local MCP 的 project.list 工具列出项目,然后停止。不要修改任何文件。"

如果要测试拓扑工具,可以改成:

opencode --pure run --format json --print-logs --log-level INFO --model openai-gw/gpt-5.4 --agent build --dir "C:\Guangyu\Projects\instrument-data-mcp" "调用 instrument-config-local MCP 的 topology.group_list 和 topology.list。参数 project_key=topo-test。只用一句话总结分组数量、拓扑数量,以及前 3 个 topology_id。不要修改文件,不要做额外操作。"

建议的测试顺序

  1. 先确认本地 MCP 服务已启动
  2. 再确认 opencode db path 指向临时目录
  3. 再测试 project.list
  4. 再测试 topology.group_list / topology.list
  5. 最后测试 topology.find_context / topology.get_node

常见错误

1. Session not found

通常不是 MCP 服务的问题,而是命令行继承了桌面版 OpenCode 注入的环境变量。

优先检查:

  • 是否清除了 OPENCODE_SERVER_PASSWORD
  • 是否清除了 OPENCODE_SERVER_USERNAME
  • 是否清除了 OPENCODE_CLIENT
  • 是否清除了 OPENCODE_PID
  • opencode db path 是否真的指向临时目录

2. 401 Invalid secret key

说明隔离环境里没有可用的 auth.json,或者 openai-gw 凭证没有被复制到隔离数据目录。

优先检查:

  • C:\Users\Guangyu\.local\share\opencode\auth.json 是否存在
  • 是否已复制到 $root\.local\share\opencode\auth.json

3. 命令行能启动,但 MCP 没连接

优先检查本地 MCP 是否真的在监听:

Invoke-WebRequest -UseBasicParsing 'http://127.0.0.1:8500/mcp' -Method Post -ContentType 'application/json' -Body '{}'

只要不是“无法连接到远程服务器”,通常说明服务已经起来了。

不建议的做法

不要直接在桌面版 OpenCode 已打开的同一个环境里运行:

opencode run ...

尤其不要在未隔离 USERPROFILE/HOME/XDG_DATA_HOME 的情况下直接测试,否则有机会干扰当前 GUI 会话数据库或命中桌面版 session 状态。

常见问题

1. project_key not found

说明 sys_config 中没有对应的 project_key

2. project 'xxx' is disabled

说明项目配置存在,但 enabled=false

3. login failed

重点检查:

  • base_url
  • username
  • password
  • 上游登录接口是否可访问

4. upstream returned non-JSON response

说明上游接口返回的不是 JSON,通常需要检查:

  • 域名或路径是否错误
  • 网关是否拦截
  • 登录态是否失效

开发说明

核心文件:

  • instrument_config_mcp/auth.py
  • instrument_config_mcp/db.py
  • instrument_config_mcp/config_api.py
  • instrument_config_mcp/server.py

语法校验:

uv run --python 3.13 python -m compileall .