# 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 下的兼容问题。 默认数据库地址: ```text sqlite:///llm_proxy.db ``` 默认上游超时: ```text UPSTREAM_REQUEST_TIMEOUT=60 ``` ## 项目配置 服务会从 `sys_config` 中读取 key 为 `mcp_project_data_projects` 的 JSON 数组。 示例: ```json [ { "project_key": "dev-01", "project_name": "DEV开发环境", "base_url": "http://192.168.1.109:32080", "username": "admin", "password": "123456", "enabled": true } ] ``` ## 安装 在项目根目录执行: ```bash uv sync --python 3.13 ``` ## 启动 默认以 HTTP 模式启动,默认地址:`http://127.0.0.1:8500/mcp` 方式一:使用模块启动 ```bash uv run --python 3.13 python -m instrument_config_mcp ``` 方式二:使用 console script 启动 ```bash uv run --python 3.13 instrument-config-mcp ``` 可选环境变量: - `MCP_HOST`,默认 `127.0.0.1` - `MCP_PORT`,默认 `8500` - `MCP_PATH`,默认 `/mcp` 如果需要指定数据库: ```bash set DATABASE_URL=sqlite:///llm_proxy.db uv run --python 3.13 python -m instrument_config_mcp ``` 如果需要指定 HTTP 地址: ```bash 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 ``` 如果需要指定上游超时: ```bash 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.nodes` 和 `diagram.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` 同时承载 `meter` 和 `device` 两类实体关系 - 刷新时间保存在分组表和拓扑注册表中 - 缓存刷新采用整批删除后重建的方式,不单独维护刷新状态表 ## 拓扑工具说明 ### `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, include_siblings=True, include_children=True)` 用途: - 获取一个节点及其直接邻域 输出结构: - `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)` 用途: - 按实体快速反查命中的拓扑节点上下文 输出结构: - `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` 返回的是一个节点的直接邻域: - 直接父节点 - 直接子节点 - 同父兄弟节点 它不做深层遍历。 ### 实体上下文查询 `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` 示例说明: ```json { "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:查询位置第一页 ```bash uv run --python 3.13 python scripts/smoke_test.py list-locations --project-key dev-01 --keyword F1 ``` 示例 2:查询系统树 ```bash uv run --python 3.13 python scripts/smoke_test.py list-system-tree --project-key dev-01 ``` 示例 3:查询设备类型 ```bash uv run --python 3.13 python scripts/smoke_test.py list-device-types --project-key dev-01 ``` 示例 4:按位置搜索仪表 ```bash 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:按系统和设备类型搜索设备 ```bash uv run --python 3.13 python scripts/smoke_test.py search-devices --project-key dev-01 --system-ids 21 --device-type-ids 5 ``` 示例 6:查询某个仪表下的点位 ```bash uv run --python 3.13 python scripts/smoke_test.py search-points --project-key dev-01 --id 1785 --page-num 1 ``` ## 联调建议顺序 1. 先运行 `list-device-types` 和 `list-meter-types`,确认认证链路正常 2. 再运行 `list-locations` 或 `list-system-tree`,确认配置接口可访问 3. 最后运行 `search-devices`、`search-meters` 或 `search-points` 做正式搜索 ## 常见问题 ### 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` 语法校验: ```bash uv run --python 3.13 python -m compileall . ```