> ## Content Index
> Fetch the complete content index at: https://www.hubwiz.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# OPC UA MCP 上手实测：AI 读 PLC 数据、浏览节点、写设定值
- URL: https://www.hubwiz.com/blog/opcua-mcp-hands-on/
- Published: 2026-10-23T03:30:01.000Z
- Updated: 2026-10-23T07:54:22.000Z
- Description: 装好之后，AI 能自己浏览节点树、批量读点位、改设定值——我们记下了完整的配置、真实返回和该守的规矩。
- Author: admin
- Tags: OPC UA, MCP, PLC, 工业自动化, Modbus

**一句话结论**：`kukapay/opcua-mcp` 和 `kukapay/modbus-mcp` 这两个 MIT 许可的 MCP Server，可以让你在 Claude Desktop 或 Cursor 里直接用自然语言读 PLC 变量、浏览节点树、改设定值。我们在本地起了 OPC UA 和 Modbus 两台模拟设备，把 11 个工具全部跑通，握手 1.3–1.9 秒、单次调用 0.01–0.03 秒。下面是完整配置、真实返回，以及上线前该守的几条规矩。

**披露（先说口径）**：本文所有数字来自 **Windows 10 + Python 3.10.6** 的干净 venv，被控对象是**本地模拟器**（OPC UA 用 `asyncua`，Modbus 用 `pymodbus`），**没有接真实 PLC**。所以本文回答的是"工具怎么装、怎么配、跑起来什么样"，不回答"在你那台设备上延迟多少"。

---

## 一、这两个 MCP Server 能干什么

| 能力     | opcua-mcp                     | modbus-mcp                              |
| ------ | ----------------------------- | --------------------------------------- |
| 读单个变量  | read\_opcua\_node             | read\_register / read\_input\_registers |
| 批量读    | read\_multiple\_opcua\_nodes  | read\_multiple\_holding\_registers      |
| 写单个值   | write\_opcua\_node            | write\_register / write\_coil           |
| 批量写    | write\_multiple\_opcua\_nodes | —                                       |
| 浏览地址空间 | browse\_opcua\_node\_children | —（Modbus 只有寄存器号）                        |

一句话区分：**OPC UA 这条路带语义、跨厂商，能看能改；Modbus 这条路快而直接，只认寄存器号。** 前者适合"让 AI 看懂现场"，后者适合"让 AI 读一块固定的点位表"。

## 二、安装：三条命令

```bash
python -m venv venv
venv/Scripts/pip install "mcp[cli]<2" asyncua "pymodbus<3.10" python-dotenv "opcua>=0.98.13" "cryptography>=45"

```

**关于版本**：这两个仓库写于 2025 年，`mcp` 与 `pymodbus` 之后都发过不兼容的大版本。所以安装时把上界钉住（`mcp<2`、`pymodbus<3.10`），装完就能直接用；如果你习惯用 `uv`，`uv sync` 会按仓库自带的锁文件走，同样没问题。

最终实测版本：

| 包                        | 版本      |
| ------------------------ | ------- |
| mcp                      | 1.30.0  |
| pymodbus                 | 3.9.2   |
| asyncua（当模拟器）            | 2.1.0   |
| opcua（python-opcua，客户端库） | 0.98.13 |

## 三、起一台模拟设备（10 秒）

OPC UA 侧，用 `asyncua` 起一台只有 4 个变量的"虚拟产线"，方便先跑通再换真设备：

```python
import asyncio
from asyncua import Server, ua

async def main():
    server = Server()
    await server.init()
    server.set_endpoint("opc.tcp://127.0.0.1:4840/freeopcua/server/")
    idx = await server.register_namespace("http://hubwiz.com/demo")
    line1 = await (await server.nodes.objects.add_object(idx, "Demo")).add_object(idx, "Line1")
    await line1.add_variable(idx, "Temperature", 36.5, ua.VariantType.Double)
    sp = await line1.add_variable(idx, "Setpoint", 40.0, ua.VariantType.Double)
    run = await line1.add_variable(idx, "Run", True, ua.VariantType.Boolean)
    for n in (sp, run):
        await n.set_writable()
    async with server:
        await asyncio.sleep(3600)

asyncio.run(main())

```

跑起来会打印 4 个 node id（本文测试里是 `ns=2;i=3`\~`ns=2;i=6`）。Modbus 侧同理，用 `pymodbus` 起一台 TCP 从站（从站号 1、保持寄存器 0–9 = 10–19、输入寄存器 = 100–109）即可，`modbus-mcp` 的 README 里有完整参数。

## 四、接到 Claude Desktop / Cursor

```json
{
  "mcpServers": {
    "opcua-mcp": {
      "command": "python",
      "args": ["C:/path/to/opcua-mcp/main.py"],
      "env": { "OPCUA_SERVER_URL": "opc.tcp://127.0.0.1:4840/freeopcua/server/" }
    },
    "modbus-mcp": {
      "command": "python",
      "args": ["C:/path/to/modbus-mcp/src/modbus_mcp/cli.py"],
      "env": { "MODBUS_TYPE": "tcp", "MODBUS_HOST": "127.0.0.1", "MODBUS_PORT": "502" }
    }
  }
}

```

重启客户端，工具列表里就会出现 `read_opcua_node` 这一组。之后你要做的只是开口问：

- *"读 ns=2;i=3 的值"*
- *"把 ns=2;i=4 设成 42.5，然后读回来确认"*
- *"浏览 Objects 下面有哪些节点"*
- *"读 ns=2;i=3、i=4、i=5 三个值，告诉我哪个超了 40"*

## 五、实测：5 个 OPC UA 工具的真实返回

| 阶段                              | 耗时              |
| ------------------------------- | --------------- |
| 首次 initialize()（起进程 + 连 OPC UA） | **1.25–1.86 s** |
| list\_tools                     | 0.01 s          |
| 单次读 / 写 / 浏览                    | **0.01–0.03 s** |

原样摘录：

```
read_opcua_node(ns=2;i=3)          -> Node ns=2;i=3 value: 36.5
read_multiple_opcua_nodes(3 个)     -> {'ns=2;i=3': 36.5, 'ns=2;i=4': 40.0, 'ns=2;i=5': True}
browse_opcua_node_children(ns=0;i=85) ->
  [{'node_id': 'i=31915', 'browse_name': '0:Locations'},
   {'node_id': 'i=2253', 'browse_name': '0:Server'},
   {'node_id': 'ns=2;i=1', 'browse_name': '2:Demo'}]
write_opcua_node(ns=2;i=4, "42.5") -> Successfully wrote 42.5 to node ns=2;i=4

```

读回来验证过：`Setpoint` 从 40.0 变成 42.5，`Run` 从 `True` 写成 `False` 再写回 `True`，都按预期生效。

三个使用习惯，能让它更好用：

1. **先 browse，再 read。** 工具返回的是给人看的字符串（`browse` 甚至是 Python `repr` 风格），先让模型看清有哪些节点、节点 ID 长什么样，再让它读值，比直接丢一个 node id 稳。
2. **批量读优先。** 要一次看十几个点位，用 `read_multiple_opcua_nodes` 一次拿完，比逐点问快，也让模型的上下文干净。
3. **写完让它读回来。** 工业场景里"写成功"和"值真的变了"是两件事，养成回读习惯，报告里也要求它贴回读值。

### 一个真实用法：每日巡检

装好之后最先能落地的是"每天早上一份巡检报告"。提示词可以固定成这样：

> 读 `ns=2;i=3`（温度）、`ns=2;i=4`（设定值）、`ns=2;i=5`（运行状态）三个值， 温度超过设定值 5 度就标红，并列出可能原因；运行状态是 False 时单独提示。

模型会自己决定调一次 `read_multiple_opcua_nodes` 拿三个值，再按你给的规则判断。相比写一个定时脚本，好处是**规则用自然语言改**——下周想把阈值从 5 度改成 3 度，改一句话就行，不用改代码、不用重新部署。

## 六、Modbus 侧：6 个工具，同样一次跑通

| 调用                                             | 返回（原样）                                                 |
| ---------------------------------------------- | ------------------------------------------------------ |
| read\_register(5)                              | Slave 1, Register 5 Value: 15                          |
| write\_register(5, 777)                        | Successfully wrote 777 to register 5 on slave 1        |
| read\_register(5)                              | Slave 1, Register 5 Value: 777 ✅                       |
| read\_input\_registers(0, count=3)             | Slave 1, Input Registers 0 to 2: \[100, 101, 102\]     |
| read\_multiple\_holding\_registers(0, count=4) | Slave 1, Holding Registers 0 to 3: \[10, 11, 12, 13\]  |
| write\_coil(2, True)                           | Successfully wrote True to coil 2 on slave 1           |
| read\_coils(0, count=4)                        | Slave 1, Coils 0 to 3: \[False, False, True, False\] ✅ |

延迟同样是 **0.01 s 量级**（本机回环）。Modbus 没有地址空间，AI 无法"自己探索有什么"，所以用之前把点位表写进提示词（或者做成一张对照表挂在项目里），比让它猜要靠谱得多。

读错了地址时，返回会是一个标准的 Modbus 异常：

```
Error reading register 999 from slave 1:
ExceptionResponse(dev_id=1, function_code=131, exception_code=2)

```

`exception_code` 记住两个就够用：**2 = 非法数据地址**（点位表对不上），**4 = 从站故障 / 网关目标设备无响应**（线或从站的问题）。在提示词里把这两句教给模型，它排查时会准确很多。

### 点位表怎么给模型

Modbus 没有地址空间，模型只能靠你给的对照表。最省事的做法是在项目说明（或 Claude 的 Project、Cursor 的 rules 文件）里放一张表：

| 地址     | 类型     | 含义     | 单位    | 可否写 |
| ------ | ------ | ------ | ----- | --- |
| HR 0   | uint16 | 产线速度设定 | rpm   | ✅   |
| HR 5   | uint16 | 当前产量   | 件     | ❌   |
| IR 0   | uint16 | 电机温度   | 0.1 ℃ | ❌   |
| Coil 2 | bool   | 启动允许   | —     | ✅   |

有了这张表，模型问"HR 5 是多少"时会直接映射到 `read_register(5)`，不会再瞎猜地址。OPC UA 侧虽然能 browse，但**把节点 ID 和业务含义对上**同样值得写一张表——browse 只能告诉你节点叫什么，告诉不了你它代表什么。

## 七、多这一层值不值

直说：**如果只是自己跑一次脚本，别用 MCP，直接调库更省事。**

```python
from opcua import Client
c = Client("opc.tcp://127.0.0.1:4840/freeopcua/server/")
c.connect()
print(c.get_node("ns=2;i=3").get_value())

```

那 MCP 这层买到了什么？

| 买到的              | 具体是什么                                                  |
| ---------------- | ------------------------------------------------------ |
| **给不写代码的人用**     | 工程师在 Claude Desktop / Cursor 里说"读一下 3 号节点的温度"，不用打开 IDE |
| **给 agent 自主编排** | 工具名 + 参数描述就是模型的说明书，它能自己决定先 browse 再 read、还是批量读         |
| **跨客户端复用**       | 一份 mcpServers 配置，Claude / Cursor / 自研 agent 都能挂        |

判断标准很简单：**"读产线"会不会变成 agent 的日常动作？** 会，这层值得，把下面那几条规矩补上；只是偶尔查一次，写脚本。

## 八、那什么时候用 OPC UA，什么时候用 Openness？

| 你想让 AI 做的事         | 该走哪条路                 | 为什么                                             |
| ------------------ | --------------------- | ----------------------------------------------- |
| 看当前值、看趋势、看报警文本     | **OPC UA**            | 标准协议、跨厂商，变量自带语义与类型；只读时风险最低                      |
| 一次采集几十个点位做分析       | **OPC UA**（批量读）       | 一次调用拿一批值，比逐点问快                                  |
| 改一个设定值 / 置位一个线圈    | OPC UA 或 Modbus（配白名单） | 能做，但要管住范围并回读校验                                  |
| 改程序：建块、写 SCL、编译、下载 | **Openness（MCP 网关）**  | OPC UA 不碰组态和程序，这件事只有 TIA Portal 的 Openness 接口能做 |
| 把老项目的 LAD 迁成 SCL   | Openness              | 要读工程结构、生成并编译代码，属于"改工程"而不是"改数据"                  |

一句话：**OPC UA 管"看和改数据"，Openness 管"改工程"。** 两条路不是替代关系，很多现场最后两个都要：运行期用 OPC UA 观测，工程期用 Openness 变更。

这条路上我们自己的产品是 [TiaLink](https://tia.hubwiz.com/tia-link?ref=hubwiz.com)：把 TIA Portal 的 Openness 能力包成 MCP 接口，AI 可以建块、写 SCL、编译、下载到 PLCSIM 再读回核对（**支持 V19 / V20，不支持 V21**）。和本文测的 `opcua-mcp` 正好互补——一个动数据，一个动工程。只需要"让 AI 看见产线在发生什么"，读完本文直接用 `opcua-mcp` 就够了。

## 九、加一层写白名单：约 20 行，实测有效

默认情况下这两个 Server 允许写任意节点。上线前最划算的一步改造，是给写操作加一张白名单——改 20 行，收益很大。以 `opcua-mcp` 为例：

```python
ALLOWED_WRITE = {"ns=2;i=4", "ns=2;i=5"}   # 只允许改设定值和运行位

def _deny(node_id: str) -> str:
    return f"已拒绝：节点 {node_id} 不在写入白名单内（ALLOWED_WRITE）"

```

单节点写在取 client 之前先判一次：

```python
if node_id not in ALLOWED_WRITE:
    return _deny(node_id)

```

批量写要**先整体校验再动手**，否则会出现"前两个写进去、第三个被拒"的半成品状态：

```python
denied = [i.get("node_id") for i in nodes_to_write if i.get("node_id") not in ALLOWED_WRITE]
if denied:
    return f"已拒绝：以下节点不在写入白名单内：{denied}"

```

我们按这个改法跑了一遍（补丁在 `distribution/hubwiz-seo/opcua-mcp-test/main_guard.py`），实测结果：

| 调用                      | 返回                                                            |
| ----------------------- | ------------------------------------------------------------- |
| 写白名单内的 ns=2;i=4 \= 41.5 | Successfully wrote 41.5 to node ns=2;i=4 ✅                    |
| 写白名单外的 ns=2;i=6 \= 99   | 已拒绝：节点 ns=2;i=6 不在写入白名单内（ALLOWED\_WRITE） ✅                    |
| 批量写 \[i=4→43.0, i=6→7\] | 已拒绝：以下节点不在写入白名单内：\['ns=2;i=6'\]，回读 i=4 **仍是 41.5** ✅ 没有产生部分写入 |

同一套思路可以直接搬到 `modbus-mcp`：把允许写的寄存器号做成集合，写工具开头判一次即可。

## 十、接进每日巡检（可选）

MCP 客户端本身不负责定时，定时交给系统：每天早上 7:00 让 agent 跑一次上面那段提示词，把结果写进一个文件或发到群里。做法有两种：

- **有 CLI 的客户端**：用 `claude -p "（提示词）" --mcp-config mcp.json` 之类的一次性调用，套 `cron` / 计划任务。
- **自研 agent**：用 `mcp` 官方 Python SDK 起一个 stdio 会话，脚本里 `call_tool` 就行（本文的测试探针 `mcp_probe.py` 就是这么写的，可以直接当模板）。

两种都建议把"昨天同一时刻的值"一起读出来做对比，否则单点数值对工程师没有信息量——**趋势比快照有用**。

## 十一、上线前的五条规矩

这五条是工业现场的通用做法，换任何一套 AI 接 PLC 的方案都适用：

1. **只给只读账户**：能只读就别给写权限；OPC UA 侧用受限角色，别用匿名。
2. **节点白名单**：把允许写入的 `node_id` 列成清单，清单外的拒绝。控制位、安全位永远不进清单。
3. **写后回读**：每次写成功都读一次比对，不一致就报错给模型，别让它自己宣布完成。
4. **审计落盘**：谁、什么时候、对哪个节点、写了什么、回读是什么，四行日志。
5. **先在仿真器上跑**：本文的模拟器脚本就够用，别拿真设备当第一次实验对象。

## 十二、四个常见问题

**Q1：能同时接多台 PLC 吗？** 可以，方式是**一个设备一个 MCP 进程**：`opcua-mcp` 的一个实例读一个 `OPCUA_SERVER_URL`，所以在 `mcpServers` 里配两个条目（`opcua-line1` / `opcua-line2`）即可，Claude 会看到两套工具。如果嫌工具名重复，把 `main.py` 里的 `FastMCP("OPCUA-Control")` 改成不同名字更清楚。

**Q2：中文变量名、结构体、DB 块怎么办？** 结构体在 OPC UA 里就是一棵子树：`browse` 到父节点，再逐个读成员即可，不用特殊处理。中文变量名同理——返回值是字符串，中文能正常显示。**建议先在仿真器上把你们真实的节点树导一遍**，确认命名空间索引（`ns=2` 还是 `ns=3`）和成员层级，再换真设备。

**Q3：能不能只订阅变化，不轮询？** 这两个工具是请求-响应式的，没有订阅接口。需要"值变了就通知"的场景，用 OPC UA 原生的 subscription（Python 里 `create_subscription` \+ `subscribe_data_change`），或者退一步用定时轮询 + 变化检测。

**Q4：安全上要注意什么？** 这两个 Server 建的是匿名连接（`Client(url)` 不设用户/证书）。所以它们的合理位置是**内网、仿真环境、或者你信任的工程机**；生产网段建议加一层：设备侧启用用户/证书鉴权、网络侧走 VPN 或跳板，再把 MCP 进程限制在固定网段。

## 十三、来源与许可

- `kukapay/opcua-mcp`：MIT，Python，31 stars，最后提交 2025-10-29
- `kukapay/modbus-mcp`：MIT，Python，25 stars，最后提交 2025-05-12
- 协议与规范：[OPC Foundation](https://opcfoundation.org/about/opc-technologies/opc-ua/?ref=hubwiz.com)（OPC UA 即 IEC 62541）、[Modbus Organization](https://modbus.org/?ref=hubwiz.com)
- 模拟器：`asyncua`（OPC UA）、`pymodbus`（Modbus TCP）
- 本文所有返回值均为本机原样摘录；未接真实 PLC，如与你的设备行为不同，以设备为准

**免责**：写操作会真的改变现场状态，上产线前请走完整的变更审批与回滚演练。

---

> 想把 AI 从"读数据"推到"改工程"，可以看 [PLC 编程 AI 软件：7 个必备的 MCP Server](https://www.hubwiz.com/blog/top7-mcp-servers-for-plc-programming/) 和 [博途 AI 助手：让 AI 直接操作 TIA Portal（TiaLink）](https://www.hubwiz.com/blog/tialink-give-your-ai-agent-hands-to-operate-tia-portal/)；本专题的其它文章在 [AI × 工业 / PLC](https://www.hubwiz.com/blog/ai-industrial-stack/)。

> **这是「AI × 工业 / PLC」专题的一部分。**完整地图见：[AI × 工业 / PLC 完全指南](https://www.hubwiz.com/blog/ai-industrial-stack/)