MCP 已经接进 PLC:但写设定值这一步,必须由人按下
agent 能读产线数据这件事已经发生了;能不能写,取决于服务器代码里那几行限额,以及有没有人替它按下按钮。
2026 年,Ignition、CODESYS 和 OPC UA 都有了 MCP server,Rockwell 和西门子则拿出了工程侧的 agent。原文给出一台在产线上试过的 Python MCP server:从 tag 白名单读,设定值只能变成提案,由人批准后才回写 PLC——以及这样一台 server 在厂里该放在什么位置。
10 月 21 日,Rockwell Automation 发布 FactoryTalk Conductor,一个用来在从设计到测试的工程工作中协调 AI agent 的平台。它为这一年收尾:AI agent 从聊天窗口搬进了自动化工具——PLC 开发环境、SCADA 平台、OPC UA server。这些集成大多说同一种协议:Model Context Protocol(MCP)。
所以对一座厂来说,问题不再是"agent 会不会拿到产线数据",而是按什么条件拿。下面先梳理 2026 年变了什么,再给出一台跑得通的产线 MCP server:agent 读指定的 tag,只能提一条设定值改动建议,改由人做。
一、2026 年带来了什么
自动化工具里的 MCP server。 Inductive Automation 在 2 月 13 日把 Ignition 的 MCP 模块放出来做 early access。在 Ignition 8.3.5 发布之前它需要 nightly build,访问由权限和 API token 管,厂商也直说这个版本不带支持。CODESYS 给自己的开发系统出了 MCP server:4 月 28 日的 1.0,7 月 22 日的 1.1。按它的数据表,工具能浏览工程树、读取并替换 Structured Text、返回设备与 I/O 组态、检查错误。OPC Foundation 在 10 月 7 日把 OPC UA MCP server 的稳定版 2.0.0 发到 NuGet,五天后跟了 2.0.1。它把 OPC UA Part 4 的服务调用暴露成 MCP 工具,工具集可以用 profile(比如 --profile core)收窄。
工程 agent。 西门子一直在扩 Eigen Engineering Agent。6 月在巴黎 VivaTech 上,它加了两个能力:从 ECAD 文件生成 PLC 标签,以及从一句话的机器描述生成符合 Siemens Automation Framework 的项目。西门子报的数字是 19 个国家 100 多家客户、比手工快 2 到 5 倍——这是厂商口径,未核实。Rockwell 计划 2027 年一季度推出 FactoryTalk Conductor,从 Studio 5000 Logix Designer、Logix Echo、Emulate3D 和 FactoryTalk Optix 起步,客户可以接自己的语言模型,包括本地部署的。
标准。 4 月 20 日,OPC Foundation 表示要把 430 多个 OPC UA companion specification 转成适合 RAG 与 MCP 的格式。MCP 自己在 7 月 28 日出了新版规范:不再有协议 session,也不再有 initialize 握手,每个请求自带协议版本与客户端能力。

背景数字。 Rockwell 的 2026 State of Smart Manufacturing 报告覆盖 17 个国家 1,500 多家制造商,它给出的 AI 支撑运营占比是今天 34%,2030 年过半。同一份调查里,46% 的企业至少出过一次网络事件。
二、为什么"把助手接到 PLC"不够
MCP 规范说得很直接:应当始终有人在回路里,并且这个人有能力拒绝工具调用。客户端必须把 readOnlyHint 这类工具注解当作不可信,除非它来自可信服务器。服务器要校验输入、执行访问控制、限流、清洗输出;客户端要记录工具调用以备审计。
网络安全机构的措辞更进一步。CISA、澳大利亚 ACSC 等九个机构 2025 年 12 月 3 日的联合指引要求:限制 AI 在没有人在回路的情况下主动控制 OT 基础设施。它也倾向 push 或 broker 架构——数据离开 OT 时不保留常驻入站通道,跨进业务网时用单向传输和可审计的暂存区。
由此得到三条规则,下面的例子全部遵守:
- agent 看到的是 tag 名单,不是控制器。 服务器拒绝任何不在名单上的名字。
- 一次写就是一条提案。 agent 的工具只把请求排进队列,由操作员运行的另一个程序写入 PLC。
- 限额由服务器执行,不由模型执行。 设定值范围和权限写在服务器代码里,不写在工具描述里。
三、MCP server 该放在哪

在生产环境里,MCP server 根本不该和控制器说话。 产线数据经网关、MQTT Sparkplug B 到 broker,再落进 OT 网外的时间序列数据库,agent 读的是那份副本。OmniMES 就是这么建的:系统从不连控制器,遥测走 MQTT Sparkplug B 落到 TimescaleDB,MCP server 用的就是这份数据。设定值改动回 OT 只有一条路——经过人。
下面的例子直连 OPC UA,因为那是在试验台上最快能跑起来的方式。规则不变,只是读的来源变了。
四、一步一步做一台产线的 MCP server
我们在 Python 3.11 上、用 mcp 2.3.0 和 asyncua 2.1.0 测过这段代码,对着的是一台带 4 个过程变量和 300 个诊断寄存器的 OPC UA 模拟器。
pip install "mcp==2.3.0" "asyncua==2.1.0"
4.1 服务器:从名单读,以提案写
"""产线 1 的 MCP server:只读白名单内的 tag,写只产出待人批准的提案。"""
import sqlite3
import time
import uuid
from asyncua import Client as OpcClient
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import ToolAnnotations
ENDPOINT = "opc.tcp://127.0.0.1:4840/line1/"
DB = "proposals.db"
# agent 只看得见这些 tag,控制器地址空间里的其他东西它看不到
READABLE = {
"press1.state": "ns=2;s=Press1.State",
"press1.count": "ns=2;s=Press1.Count",
"press1.oil_temp": "ns=2;s=Press1.OilTemp",
"press1.pressure_setpoint": "ns=2;s=Press1.PressureSetpoint",
}
# agent 可以提议的设定值,硬限位在服务器侧校验
PROPOSABLE = {"press1.pressure_setpoint": (150.0, 200.0)}
mcp = MCPServer("line1")
_opc = None
async def opc():
global _opc
if _opc is None:
_opc = OpcClient(ENDPOINT)
await _opc.connect()
return _opc
def db():
con = sqlite3.connect(DB)
con.execute(
"create table if not exists proposals (id text primary key, tag text, value real, reason text,"
" created real, status text, approved_by text, executed real, before real, after real)"
)
return con
@mcp.tool(annotations=ToolAnnotations(read_only_hint=True, open_world_hint=False))
async def read_tags(tags: list[str]) -> dict:
"""读取产线 1 这些 tag 的当前值。
允许:press1.state、press1.count、press1.oil_temp、press1.pressure_setpoint。
"""
unknown = sorted(set(tags) - READABLE.keys())
if unknown:
raise ToolError(f"Tags not allowed: {unknown}. Allowed: {sorted(READABLE)}")
client = await opc()
result = {}
for tag in tags:
dv = await client.get_node(READABLE[tag]).read_data_value()
stamp = dv.SourceTimestamp or dv.ServerTimestamp
result[tag] = {
"value": dv.Value.Value,
"status": dv.StatusCode.name,
"time": stamp.isoformat() if stamp else None,
}
return result
@mcp.tool(annotations=ToolAnnotations(read_only_hint=False, destructive_hint=False, open_world_hint=False))
async def propose_setpoint(tag: str, value: float, reason: str) -> dict:
"""把一条设定值改动排队,等人批准。在那之前产线上什么都不会变。"""
if tag not in PROPOSABLE:
raise ToolError(f"No proposals allowed for {tag}. Allowed: {sorted(PROPOSABLE)}")
low, high = PROPOSABLE[tag]
if not low <= value <= high:
raise ToolError(f"{value} is outside the permitted range {low}-{high}")
proposal_id = uuid.uuid4().hex[:8]
with db() as con:
con.execute(
"insert into proposals (id, tag, value, reason, created, status) values (?, ?, ?, ?, ?, 'pending')",
(proposal_id, tag, value, reason, time.time()),
)
return {"proposal_id": proposal_id, "status": "pending human approval"}
if __name__ == "__main__":
mcp.run(transport="stdio")
read_tags 带 read_only_hint=True,propose_setpoint 没有——但客户端没有义务相信注解。真正的保护是 READABLE 和 PROPOSABLE:模型看不见名单外的寄存器,也没法提一个 260 的压力,因为服务器在入队之前就把范围查了。ns=2 这个命名空间索引来自模拟器,硬编码 node id 之前要在真控制器上核对。
4.2 审批:操作员的程序,不是 agent 的
"""操作员侧:批准一条提案并写入 PLC。agent 永远不会运行这段。"""
import asyncio
import sys
import time
from asyncua import Client as OpcClient, ua
from line_mcp import ENDPOINT, PROPOSABLE, db
WRITABLE = {"press1.pressure_setpoint": "ns=2;s=Press1.PressureSetpoint"}
async def approve(proposal_id: str, operator: str):
with db() as con:
row = con.execute("select tag, value, status from proposals where id = ?", (proposal_id,)).fetchone()
if row is None or row[2] != "pending":
raise SystemExit(f"No pending proposal {proposal_id}")
tag, value, _ = row
low, high = PROPOSABLE[tag]
if not low <= value <= high: # 再查一次:数据库不是 PLC 的安全层
raise SystemExit(f"{value} is outside {low}-{high}")
async with OpcClient(ENDPOINT) as opc:
node = opc.get_node(WRITABLE[tag])
before = await node.read_value()
await node.write_value(value, ua.VariantType.Double)
after = await node.read_value()
with db() as con:
con.execute(
"update proposals set status = 'executed', approved_by = ?, executed = ?, before = ?, after = ? where id = ?",
(operator, time.time(), before, after, proposal_id),
)
print(f"{tag}: {before} -> {after}, approved by {operator}")
if __name__ == "__main__":
asyncio.run(approve(sys.argv[1], sys.argv[2]))
程序把范围再查一遍,因为提案数据库不是安全层。它同时记下改动前后的值,以及批准人是谁。在真厂里,这段应该长在带登录的 HMI 面板或应用里,而不是一个控制台脚本。
4.3 不用语言模型也能测
SDK 的客户端可以在同一个进程里连上服务器,所以整条链路不用模型也能跑:
import asyncio
from mcp import Client
import line_mcp
async def main():
async with Client(line_mcp.mcp) as client:
result = await client.call_tool("read_tags", {"tags": ["press1.oil_temp"]})
print(result.is_error, result.content[0].text)
asyncio.run(main())
接真客户端时,用 python line_mcp.py 启动服务器,它走标准输入输出通信。测试模拟器是一台 asyncua 的 OPC UA server:一个 Press1 对象(State、Count、OilTemp,以及可写的 PressureSetpoint),外加 Diagnostics 对象下的 300 个寄存器。
五、SDK 2.x 的两个坑
FastMCP 没了。 2.x 里类叫 MCPServer,从 mcp.server.mcpserver 导入。从 mcp.server.fastmcp 导入会报错,错误信息指向迁移指南——所以网上大量示例需要更新。
普通异常会把错误文本吞掉。 如果工具抛 ValueError,模型只看到 Error executing tool read_tags,永远不知道哪些 tag 是允许的。只有抛 ToolError,这条消息才会传到模型。两种都试过:用 ToolError 时,agent 能拿到允许的名字列表,并改正自己的调用。在 SDK 的结果对象里,这个字段是 is_error。
六、有多少数据会进模型

一个"把所有 tag 都给我"的工具很诱人,因为它一上来就能用。在我们的模拟器上,把控制器整个命名空间(304 个变量)导出来是 37,397 字节;读那 4 个名单内的 tag 是 465 字节,差了约 80 倍。这些字节每一个都会进模型上下文,既花钱又稀释注意力。真控制器的变量数不同,倍数会变,但方向不变。
七、给 agent 多大自由

图源:OmniMES 原文配图,随文编译。图注由译者译自原文 alt。
原文建议分四级:
| 级别 | 做法 | 适用 |
|---|---|---|
| 1 | 从 tag 名单读 | 每条产线都该从这里起步 |
| 2 | 提案由人批准 | 多数场景的合理目标 |
| 3 | 在窄限位内自动写、不经批准 | 只在做过风险评估之后,安全功能永远不行 |
| 4 | 完全自主 | 不建议用在 OT 里 |
这是原文的建议,不是标准——依据就是前面引的 CISA 指引与 MCP 规范。
八、障碍与限制
- 工具成熟度。 Ignition 的 MCP 模块是 early access 且不带支持;CODESYS 的 MCP server 不含技术支持,且同一时间只能挂一个开发环境;FactoryTalk Conductor 得等到 2027 年。
- 厂商口径。 Eigen Engineering Agent 那"2 到 5 倍"是西门子的数字,在自己的项目上量过再决定。
- 连接安全。 例子里的 OPC UA 没有加密。真厂要
SignAndEncrypt加证书、给 MCP server 一个专用账号,并在 OPC UA server 侧只给读权限。 - 数据即模型输入。 tag 值、报警文本、操作员备注,都是以文本形式进模型的。MCP 规范要求:工具结果在交给模型之前先校验。
- 例子省掉了什么。 它没有限流,也没有读操作日志——而规范要求限流;它也不重连掉线的 OPC UA 会话。用 SQLite 当队列、用脚本当操作面板,做测试可以,轮班干活不行。
九、这周可以做什么
- 列出第一个用例需要的 tag。通常是一二十个,不是几百个。
- 定下读的来源:OT 网外的历史库、broker 或数据库,不是控制器。
- 把 agent 可能提议的每个设定值写清楚:范围是多少,谁批准。
- 在模拟器上跑一遍例子,看看调用出错时模型到底收到了什么。
- 从第一天就记录工具调用——在 agent 碰到生产环境的任何人之前。
十、OmniMES 里的 MCP
OmniMES 从 4.3.0 起内置 MCP server。AI 助手用 token 连上去,可以配置机器、搭看板,而原始机器遥测保持只读。
十一、边界
- 这是厂商博客,作者所在公司在卖 MES,文中的生产架构就是他们的实现方式,有商业动机。
- 西门子"100 多家客户、快 2 到 5 倍"是厂商自报,原文明确说未核实,我们同样没核。
- 例子跑的是 OPC UA 模拟器,
ns=2取自模拟器——换到真控制器必须重核 node id。 - 37,397 与 465 字节是他们那台模拟器上的数,变量数不同倍数就不同;方向(白名单远小于全量)成立,具体倍数别当常数用。
- 时间线里的版本号与日期均按原文,这类东西迭代快,读的时候对一下官方 changelog。
我们的整理:这篇讲的是运行时数据面——agent 读产线数据、写设定值要人点头,边界落在 tag 白名单和 proposals 表上。工程面(读写博途工程、编译、跑仿真)是另一套机制:不是 tag 白名单,而是工程对象级的读写范围,加写前预演与读回核对。这一侧的路径见文末。
相关阅读:AI 改博途程序:TIA Openness MCP 的写保护与边界 | Claude Code 接博途:从空工程到跑通仿真的全链路 | AI Agent 安全:九起事故、六项控制与 MCP 供应链 | MCP 供应链体检:15,341 个服务器扫描与上线前 8 项检查 | 博途这一侧的工程读写与编译仿真,走的是 TiaLink(第三方博途 MCP 网关,非西门子官方)。
Sources
- OmniMES, MCP has reached the PLC: how to connect an AI agent to a line so it reads data and a human approves changes, 2026-11-02 —— https://www.omnimes.com/en/blog/mcp-has-reached-the-plc-how-to-connect-an-ai-agent-to-a-line-so-it-reads-data-and-a-human-approves-changes
- Rockwell Automation, Introduces FactoryTalk Conductor, an AI-Native Orchestration Platform for Industrial Automation, PR Newswire, 2026-10-21 —— https://www.manilatimes.net/2026/10/21/tmt-newswire/pr-newswire/rockwell-automation-introduces-factorytalk-conductor-an-ai-native-orchestration-platform-for-industrial-automation/2450721
- Siemens, Siemens takes AI for the physical world to the next level with two new Eigen Engineering Agent capabilities, 2026-06-17 —— https://press.siemens.com/global/en/pressrelease/siemens-takes-ai-physical-world-next-level-two-new-eigen-engineering-agent
- CODESYS, Development System MCP Server 数据表 —— https://us.store.codesys.com/media/n98_media_assets/files/2101000023/2/CODESYS%20Development%20System%20MCP%20Server_en.pdf
- SIM Solutions, AI in OT update, Q4 2026: PLC and SCADA news that matters, 2026-10-25 —— https://simsolutions.be/insights/ai-in-ot-update-2026-q4/
- NuGet, OPCFoundation.NetStandard.Opc.Ua.Mcp —— https://www.nuget.org/packages/OPCFoundation.NetStandard.Opc.Ua.Mcp
- OPC Foundation, Advances OPC UA for the AI Era with Companion Specifications Optimized for Agentic AI, 2026-04-20 —— https://opcfoundation.org/news/press-releases/opc-foundation-advances-opc-ua-for-the-ai-era-with-companion-specifications-optimized-for-agentic-ai/
- Inductive Automation, MCP Module Early Access, 论坛, 2026-02-13 —— https://forum.inductiveautomation.com/t/mcp-module-early-access/113966
- Model Context Protocol, Key Changes, revision 2026-07-28 —— https://modelcontextprotocol.io/specification/2026-07-28/changelog
- Model Context Protocol, Tools, revision 2026-07-28 —— https://modelcontextprotocol.io/specification/2026-07-28/server/tools
- MCP Python SDK, migration guide to v2 —— https://py.sdk.modelcontextprotocol.io/v2/migration/
- CISA 等九机构, Principles for the Secure Integration of Artificial Intelligence in Operational Technology, 2025-12-03 —— https://www.cisa.gov/resources-tools/resources/principles-secure-integration-artificial-intelligence-operational-technology
- Process Online, Rockwell Automation releases 2026 State of Smart Manufacturing Report, 2026-05-20 —— https://www.processonline.com.au/content/business/news/rockwell-automation-releases-2026-state-of-smart-manufacturing-report-1482753481
本文由 hubwiz 编译整理,转载请注明出处。