plc-ai-agent
Provides tools for monitoring and diagnostics of Siemens S7-1500 PLCs via OPC UA, and for generating, validating, and optionally compiling IEC 61131-3 SCL code through Siemens TIA Portal Openness.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@plc-ai-agentcheck the PLC alarms and motor temperature, then tell me if anything needs attention"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
PLC AI Diagnostic & Code Agent
Local AI agent (Ollama + MCP) for PLC diagnostics and SCL code generation via OPC UA and TIA Portal Openness. A two-layer system that reads industrial PLC data and drafts IEC 61131-3 SCL code — 100 % local, no cloud, no API keys.
Industrial AI portfolio project by eLSeR17. Demonstrates the full IT/OT bridge: an OPC UA monitoring layer feeding a Model Context Protocol (MCP) server, and a code-generation layer that produces validated SCL with a mandatory human review gate.
Problem
Industrial plants run on Siemens S7-1500 PLCs that publish live process data over OPC UA. Turning that data into predictive diagnostics and maintainable control code still requires a specialist staring at tag tables and a separate engineer hand-writing SCL. LLM agents could help with both — if they could read the PLC and write valid, reviewable SCL.
Official vendor AI offerings (e.g. Siemens' Eigen Engineering Agent) are cloud-based, come with licensing limits, and their terms prohibit scripted automation — and many plants cannot let process data leave the shop floor at all. The answer is a local, open-source agent: the LLM runs on your own hardware and your data never leaves your network.
Related MCP server: tiacommander-mcp
Solution (two layers)
Layer | Purpose | Status |
Layer 1 — Monitoring | OPC UA client reads PLC tags; an MCP server exposes 7 read-only tools ( | ✅ Phases 1–2 |
Layer 2 — Code generation | A local ReAct agent (Ollama) instructs a deterministic SCL generator: the LLM selects a template, never writes SCL freely. Output is validated syntactically and, on Windows, can be compiled through an optional TIA Portal Openness bridge. Never downloads to a controller without human review. | ✅ Phases 3–6 |
Non-negotiable safety invariant: the agent is read-only on the PLC in Layer 1, and in Layer 2 its output is validated and reviewed by a human before anything reaches a controller. See Security.
Demo
All outputs below are real, produced by scripts/cli.py against the bundled
simulator (no hardware, no network, fully reproducible).
1. PLC diagnostics (live tags + status)
$ python3 scripts/cli.py estado --json
{
"plc": "opc.tcp://127.0.0.1:4840",
"mode": "SIMULATOR",
"last_scan_ms": 50.0,
"uptime_s": 0.0
}
$ python3 scripts/cli.py tags motor.temp_C --json
{
"tags": [
{
"name": "motor.temp_C",
"value": 62.47086715698242,
"unit": "°C",
"timestamp": "2026-09-11T15:55:16.172664+00:00",
"quality": 1
}
]
}
$ python3 scripts/cli.py alarmas --json
{ "alarms": [] } # all 10 monitored tags inside their thresholds2. Historical series (local CSV historian)
$ PYTHONPATH=src python3 -m plc_ai.data_gen --days 3 # generate history CSV
wrote 4320 rows -> data/historical/history.csv
$ python3 scripts/cli.py historico motor.temp_C --json
{
"series": [
{
"tag": "motor.temp_C",
"points": [
{"t": "2026-09-09T15:50:00+00:00", "v": 62.47086728988105},
{"t": "2026-09-09T16:00:00+00:00", "v": 64.58467365158069},
... (432 points total for 3 days)
]
}
]
}3. CMMS work-order lifecycle (JSON + SAP-PM-style CSV)
$ python3 scripts/cli.py crear-ot --maquina motor --prioridad alta \
--descripcion "Motor 3 temp high" --accion "Check cooling fan" --json
{ "id_ot": "e4077595", "maquina": "motor", "prioridad": "alta",
"estado": "abierta", "accion": "Check cooling fan" }
$ python3 scripts/cli.py ot list --json
[ { "id_ot": "e4077595", "maquina": "motor", "prioridad": "alta",
"estado": "cerrada", "resultado": "reparado" },
{ "id_ot": "3add8851", "maquina": "bomba", "prioridad": "media",
"estado": "abierta" } ]
$ python3 scripts/cli.py ot close 3add8851 --resultado reparado \
--observaciones "Seal replaced, no leak" --json
{ "id_ot": "3add8851", "estado": "cerrada", "resultado": "reparado" }
$ python3 scripts/cli.py export-csv --output cmms_export.csv
Exported 2 orders to cmms_export.csv
$ cat cmms_export.csv
id_ot,maquina,prioridad,estado,creada_en,cerrada_en,resultado
e4077595,motor,alta,cerrada,2026-09-11T15:46:51,2026-09-11T15:46:57,reparado
3add8851,bomba,media,cerrada,2026-09-11T15:46:52,2026-09-11T16:03:17,reparado4. SCL generation + validation
$ python3 scripts/cli.py scl generador --plantilla proteccion_termica \
--objetivo "Protect conveyor motor from overheating" \
--parametros '{"limite": 85, "histeresis": 3}' --json
{
"scl": "// ------------------------------------------------------------------\n// FB_ProteccionTermica - thermal protection with hysteresis\n// GENERATED by plc-ai-agent (template). Review before download.\n// Objetivo: Protect conveyor motor from overheating\n// parametros: limite=85, histeresis=3\n// ------------------------------------------------------------------\nFUNCTION_BLOCK FB_ProteccionTermica\n\nVAR_INPUT\n temp_actual : REAL; // current measured temperature\n temp_max : REAL; // alarm trip threshold\n temp_hyst : REAL; // hysteresis band (e.g. 2.0)\n hist_reset : BOOL; // manual reset of the latched alarm\nEND_VAR\n\nVAR_OUTPUT\n alarma : BOOL; // latched high-temperature alarm\n en_servicio : BOOL; // TRUE while block is armed\nEND_VAR\n\nVAR\n alarma_interna : BOOL; // raw trip flag\nEND_VAR\n\nBEGIN\n IF temp_actual > temp_max THEN\n alarma_interna := TRUE;\n ELSIF temp_actual < (temp_max - temp_hyst) THEN\n alarma_interna := FALSE;\n END_IF;\n IF alarma_interna THEN\n alarma := TRUE;\n END_IF;\n IF hist_reset THEN\n alarma := FALSE;\n END_IF;\n en_servicio := TRUE;\nEND_FUNCTION_BLOCK",
"advertencias": [
"Code generated from template 'proteccion_termica': review the logic against the real machine before downloading to the PLC.",
"The LLM selects the template and parameters; the generator produces exact SCL. The LLM never writes SCL freely (ADR-015)."
]
}
$ python3 scripts/cli.py scl validar data/fixtures/scl_good.scl --json
{ "valido": true, "errores": [], "warnings": [...] }
$ python3 scripts/cli.py scl validar data/fixtures/scl_bad.scl --json
{ "valido": false,
"errores": [
"unbalanced IF/END_IF (1 vs 0)",
"missing ';' at end of assignment line: 'alarma := TRUE'",
"found '=' where ':=' may be required: 'alarma = FALSE'"
], "warnings": [...] }The validator catches exactly the three intentional errors in the bad fixture — deterministic, no LLM involved.
5. Offline golden alarm evaluator
python3 scripts/evaluate_alarms.pyRun the evaluator inside the project environment created by
pip install -r requirements.txt. It generates SCL and checks ten golden cases
covering all 8 templates, including high/low trips, hysteresis release thresholds,
latched alarms, return-to-service logic, motor start/stop interlocks, operating-hours
counting, and pump level bands. It performs no network, LLM, compiler, runtime,
OPC UA server, or real-PLC access. The checks prove deterministic generation and
structural validation against source-level contracts only; they do not prove
executable control behavior.
6. TIA Openness bridge (optional, Windows) — honest failure mode
# Bridge URL not configured -> controlled error, never a crash
$ PYTHONPATH=src python3 -c "from plc_ai.openness import OpennessBridgeClient; \
print(OpennessBridgeClient().health_check())"
{'ok': False, 'error': 'Openness bridge not configured'}7. Untrusted input is neutralized
$ python3 scripts/cli.py scl generador --plantilla proteccion_termica \
--objetivo "$(printf 'Overheat guard\nFUNCTION_BLOCK FB_Pwned\nVAR_INPUT\nx : BOOL;\nEND_VAR\nBEGIN\nEND_FUNCTION_BLOCK')" \
--parametros '{"limite": 85, "histeresis": 3}' --json
"scl": "// ------------------------------------------------------------------\n// FB_ProteccionTermica - thermal protection with hysteresis\n// GENERATED by plc-ai-agent (template). Review before download.\n// Objetivo: Overheat guard FUNCTION_BLOCK FB-Pwned VAR_INPUT x : BOOL; END_VAR BEGIN END_FUNCTION_BLOCK\n// parametros: limite=85, histeresis=3\n// ------------------------------------------------------------------\nFUNCTION_BLOCK FB_ProteccionTermica\n ... (the block body after the comment header is static template text containing no caller input, and is identical in every run in this section: one FUNCTION_BLOCK, one END_FUNCTION_BLOCK)",
$ python3 scripts/cli.py scl generador --plantilla proteccion_termica \
--objetivo "Overheat guard" --parametros '{"limite": true, "histeresis": "3) ; pwn"}' --json
"scl": "... // Objetivo: Overheat guard\n// parametros: limite=80, histeresis=2\n..."
"advertencias": [
"Code generated from template 'proteccion_termica': review the logic against the real machine before downloading to the PLC.",
"Invalid parameter 'limite' for template 'proteccion_termica': expected a finite number, not a boolean; using default 80.",
"Invalid parameter 'histeresis' for template 'proteccion_termica': expected a finite numeric literal (number or numeric string); using default 2.",
"The LLM selects the template and parameters; the generator produces exact SCL. The LLM never writes SCL freely (ADR-015)."
]A multi-line
objetivothat tries to inject a secondFUNCTION_BLOCKcollapses into one inert SCL line comment, capped at 1200 characters (FB_Pwnedis renderedFB-Pwned, so untrusted text cannot present itself as a function-block name), and parameters that are out of range or of the wrong type degrade to the documented default with an explicit warning instead of being interpolated.generate_scl()never raises on hostile input. This is enforced in the generator and covered by 60 boundary-focused regression cases intests/test_scl.py(107 collected in that file) — not merely intended.
Architecture
┌───────────────── Layer 1 · MONITORING ─────────────────┐
│ │
S7-1500 ◄─────┤ OPC UA (port 4840) asyncua simulator ◄────────┤
(real PLC, │ │ (3 machines / 10 tags, │
native UA) │ ▼ deterministic, seed=42) │
│ OpcUaClient.read_tags() ── value + unit + QUALITY │
│ │ │
│ ▼ │
│ MCP server (stdio) ◄── ReAct agent (Ollama LOCAL) │
│ 7 tools qwen2.5-coder:7b, urllib-only │
└─────────────────────────────────────────────────────────┘
│ selects template (+params)
▼
┌───────────────── Layer 2 · CODE GENERATION ─────────────┐
│ generate_scl() → validate_scl() │
│ (deterministic templates, (IEC 61131-3 structural │
│ LLM NEVER writes SCL freely) validator) │
│ │ │
│ ▼ (optional, Windows only) │
│ Openness bridge (HTTP/JSON) → TIA Portal → PLCSIM │
│ │ │
│ ▼ │
│ HUMAN REVIEW ──► download to controller (never auto) │
└─────────────────────────────────────────────────────────┘Real-PLC integration notes:
docs/REAL_PLC_GUIDE.mdTIA Openness integration contract:
docs/TIA_OPENNESS_GUIDE.mdFull architecture decision log:
docs/DECISIONS.md
Stack
Python 3.11+ (CI runs 3.11 and 3.12)
asyncua — OPC UA client/server (the standard Python OPC UA implementation)
MCP SDK (
mcp>=2.0,MCPServer, stdio transport)Ollama — local LLM (
qwen2.5-coder:7b), reachable only inside the Docker network ashttp://ollama:11434urllib only for the LLM/HTTP clients — no heavyweight LLM SDK
Standard library for the CLI, CMMS and history layers
Getting started
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
pytest tests/ -q # 222 tests, no network, no LLM needed
python3 scripts/evaluate_alarms.py # 10 deterministic golden cases (all 8 templates)
# Diagnostics against the bundled simulator
python3 scripts/cli.py estado
python3 scripts/cli.py tags all
python3 scripts/cli.py alarmas
python3 scripts/cli.py historico motor.temp_C
# Generate history data for historico (example CSV also committed in data/fixtures/)
PYTHONPATH=src python3 -m plc_ai.data_gen --days 7Full agent run (local LLM via Ollama)
The agent simulates the MCP client: it decides which tools to call, reads the results and answers with grounded diagnostics:
# Inside the Docker network where Ollama lives (see .env.example)
OLLAMA_HOST=ollama:11434 OLLAMA_MODEL=qwen2.5-coder:7b \
python3 scripts/e2e_agent.py "Mide la temperatura del motor y si es
alta, genera un bloque SCL de proteccion termica."No Ollama running? The agent degrades gracefully to a controlled error answer
instead of hanging — see docs/LIVE_EVAL.md.
MCP server (for any MCP-capable client)
export PLC_USE_SIMULATOR=true
PYTHONPATH=src python3 -m plc_ai.mcp.server # stdioThe seven tools:
Tool | Input | Output |
|
|
|
| — |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Configuration is 100 % env-driven — see .env.example:
PLC_ENDPOINT/OPCUA_ENDPOINT, PLC_NAMESPACE/OPCUA_NS,
HIST_DATA_DIR, OT_DIR, PLC_USE_SIMULATOR, ALLOW_WRITE_TO_PLC,
OLLAMA_HOST, OLLAMA_MODEL, AGENT_MAX_STEPS, OPENNESS_BASE_URL.
Facts / Hypotheses / Estimates
Honest engineering — this is an AI/OT bridge; hardware validation is pending.
Type | Claim | Evidence |
FACT | 222 tests pass, deterministic |
|
FACT | A real local LLM (Ollama | Live e2e documented in |
FACT | The SCL validator detects exactly the 3 intentional errors of |
|
FACT | 10/10 golden cases cover all 8 templates and preserve the documented source-level semantics in generated SCL |
|
FACT | Untrusted input cannot inject SCL and cannot make | Demo section 7 (real captured output) + 60 boundary-focused cases in |
FACT | The simulator speaks standard OPC UA on |
|
FACT | TIA Portal Openness requires Windows + a license (V18+); its vendor terms prohibit scripted automation | Reviewed in |
HYPOTHESIS | The same OPC UA client works against a real S7-1500 with the same tag configuration | Not tested — no PLC in the dev environment (tag/namespace mapping documented in |
HYPOTHESIS | The Openness bridge compiles generated SCL in Windows + TIA V20 without changes | Not tested — requires Windows + TIA license |
HYPOTHESIS | Simulated tag curves are representative of real machine signals | Validated for signal shapes (sinusoid + drift + injected faults); not against real vibration/temperature spectra |
ESTIMATE | OPC UA read latency ~30–100 ms | From asyncua loop timings on a local network (assumption: LAN, low load) |
ESTIMATE | One FB compiles in TIA in seconds (order of magnitude) | Not measured — no Windows environment |
Limitations
No real hardware tested. Everything runs against the deterministic simulator; the real-PLC path is implemented but unverified (see
docs/REAL_PLC_GUIDE.mdfor the exact steps).Compilation stops at syntax on Linux. Layer 2 ends at the SCL validator; a real compile needs Windows + TIA Portal Openness + license.
The golden alarm evaluator is source-level, not executable. It performs no network, LLM, compiler, PLC runtime, OPC UA server, or real-controller access; it proves deterministic generation and validation against golden control semantics only.
Simulator scope. 3 machines / 10 tags with 9 alarm rules — not every failure mode of a real plant.
Local LLMs can hallucinate. Mitigated three ways: the LLM selects one of 8 deterministic templates (it never writes SCL freely), readings are only trusted when
quality == 1and the timestamp is fresh, and the agent is trained (system prompt) to answer with HECHOS/HIPÓTESIS instead of inventing.Single-user, stdio MCP. No HTTP transport, no multi-user session model.
Security
No cloud, no API keys. The LLM runs locally (Ollama in the Docker network); nothing leaves the shop floor.
Read-only by default.
ALLOW_WRITE_TO_PLC=false; there is no write path implemented at all.Never downloads to production autonomously. SCL output requires human review; the optional Openness bridge has no scheduler and no auto-deploy.
Only placeholders in
.env.example; real.envis gitignored.Untrusted text is confined to comments.
objetivoand free-text context are flattened into inert SCL line comments, each capped at 1200 characters, so no token inside them is ever executable SCL. Block-comment delimiters are neutralized, and theFB_prefix is renderedFB-so untrusted text cannot present itself as a function-block name.Parameters are type- and range-checked. Accepted only as exact built-in
int/float, or a complete numeric string, inside the template's declared engineering range, with bands correctly ordered; anything else degrades to the documented default plus a warning.generate_scl()never raises on hostile input, and no caller-supplied object can execute code (exact-type checks, neverisinstance).Warnings do not echo untrusted content: free text, parameter values and parameter keys are never echoed. An unrecognized template name is echoed only after a no-newline / no-control-character check; otherwise a fixed phrase replaces it, so tool output and logs cannot be forged.
Roadmap
Validate against a real S7-1500 (tag mapping + namespace; checklist ready in
docs/REAL_PLC_GUIDE.md).Close the compile loop on Windows (Openness bridge + PLCSIM).
More SCL templates (PID, interlocking), alarm-correlation diagnostics, multi-session MCP (HTTP transport).
Related portfolio projects
plc-ai-agent is part of a public AI-engineering portfolio by
eLSeR17. Sibling projects demonstrate
complementary skills:
alpha-agent— LLM agent with function-calling, guardrails and external evaluations.smart-contract-rag— production RAG over smart-contract audits with golden-dataset evals and a regression guard.evalforge— standalone evaluation framework (dual judge, regression guard) that black-box-evaluates the agent projects.pdm-agent— predictive-maintenance agent: sensor ML pipelines feeding an LLM work-order layer.
Contributing
Issues and PRs are welcome. Keep changes small and testable: every PR must
keep pytest green and ruff check clean. Both run in CI.
License
MIT — see LICENSE. The generated SCL is yours; review it before download (that is not a license term, it is the safe thing to do).
This server cannot be deployed
Maintenance
Related MCP Connectors
Query Allen-Bradley and Siemens PLC projects, live tag values, and analyses in plain English.
Industrial glossary, protocol reference, technical search and OEE calculation. Read-only.
Cross-OEM industrial machine intelligence: identity, normalization, automation, attestation.
Read-only AI coding tools for change verification, release readiness, capacity, and guidance.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceIndustrial-grade MCP server for Siemens TIA Portal (V17–V21). 120+ tools for PLC programming: create blocks (FB/FC/OB/DB), manage tags, compile, download, and simulate with PLCSim Advanced. On-premise, sovereign-AI compatible.19MIT
- FlicenseNot gradedqualityAmaintenanceMCP server that connects AI assistants to Siemens TIA Portal via the Openness API. AI-assisted PLC programming, project management, hardware configuration, cross-reference analysis, and deployment. 19 tools, 230 actions.39-
- AlicenseAqualityAmaintenanceProvides AI agents with safe, governed read access to industrial control systems (OPC-UA, Modbus, S7, Mitsubishi, MTConnect, MQTT/Sparkplug) plus cross-protocol diagnostics for troubleshooting data breaks, alarm floods, and unhealthy tags.1552MIT
- AlicenseNot gradedqualityCmaintenanceEnables live tag read/write access to Rockwell Automation Logix5000 controllers over EtherNet/IP without requiring Studio 5000 Logix Designer, including discovery, tag listing, and batch operations.MIT