ctrlX Engineering MCP
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., "@ctrlX Engineering MCPbuild the project and show me any errors"
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.
ctrlX Engineering MCP
An MCP server that lets Claude Code drive ctrlX PLC Engineering (CODESYS V3) through the CODESYS Scripting Engine: read and write POUs, build, go online, start/stop, read and write variables, and record traces.
Claude Code --stdio--> server/server.py (CPython, FastMCP)
| JSON lines over TCP + token (SSH reverse tunnel)
v
PLC Engineering (Windows) running bridge/bridge.py (IronPython 2.7, inside the IDE)Tested with ctrlX PLC Engineering 4.6.2 (ScriptEngine 4.2.0.0) against a ctrlX CORE.
Setup
Linux / WSL (where Claude Code runs)
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
scripts/install_token.sh # creates .ctrlx_token, exports CTRLX_MCP_TOKEN from ~/.bashrc.mcp.json registers the server for Claude Code in this folder. The token is read from the
environment, so open a new terminal after installing it.
Windows (where PLC Engineering runs)
Copy this folder (at least
bridge/andscripts/) to the Windows machine.Copy the token printed by
install_token.shintoscripts\.ctrlx_token.Copy
scripts\config.example.battoscripts\config.batand edit it (Linux user and host, IDE path, optional project to open).Make SSH login to the Linux box key-based (
ssh-keygen, thenssh-copy-id), or the tunnel window waits for a password.Run
scripts\start_bridge.bat. It opens the tunnel and starts the IDE with the bridge loaded.
The bridge listens on 127.0.0.1 only, and every request needs the token. CTRLX_MCP_PORT changes
the port (default 8765), CTRLX_MCP_ALLOW_EXEC=0 disables the exec_script tool.
Check it from Linux: ask Claude to ping, or run
CTRLX_MCP_TOKEN=$(cat .ctrlx_token) .venv/bin/python -c "import sys; sys.path.insert(0,'server'); import server; print(server.call('ping'))"If the IDE misbehaves when the script loads at startup, set AUTORUN=0 in config.bat and run
the bridge from Tools > Scripting > Execute Script File. Its log is %TEMP%\ctrlx_bridge.log
(online context saved and ui dispatch ready should appear, then loop started).
Related MCP server: TIA-Portal MCP-Server
Tools
Project: ping, open_project, save_project, get_tree, get_pou, set_pou, create_pou,
create_dut, create_gvl, delete_object, build, messages.
PLCopenXML: export_plcopen, import_plcopen. Ladder: ladder_check, ladder_import, ladder_export.
Online: login, logout, run_state, plc_start, plc_stop, read_var, write_var.
Trace: trace_setup, trace_run, trace_stop, trace_wait, trace_save.
Escape hatch: exec_script (runs IronPython in the IDE; use dir() to explore the API).
Object paths are /-separated names from the project root, for example
Device/Plc Logic/Application/PRG_Main. Name matching is exact; check get_tree for the
spelling (Plc Logic vs PLC Logic).
Rules for Claude
Always ask the user how to download the application before
login. An application download can replace the program, reset variables to their initial values and stop a running application. (A trace download is a different thing, see Tracing.) Do not decide it, and do not log in "just to look" at a running machine.change="Never"stops a running controller when you log in (seen on a ctrlX CORE). The default isTry(online change), which keeps it running; it is also whattrace_runuses.To attach to a running PLC use
loginwithchange="Try". Thedownloadflag is passed to the IDE as "delete foreign applications" on the controller, needs extra rights (a permissions error otherwise) and defaults to False; leave it off unless the user asks.Always ask the user how many samples the trace should use before
trace_setup;samplesis required and is not defaulted.write_var,plc_start,plc_stopandloginaffect a running machine; confirm first.
Tracing
Scripts cannot create a trace object (trace.create returns a stub and adds nothing). Add one
once in the IDE: right-click the Application > Add Object > Trace. After that:
trace_setup trace="Device/Plc Logic/Application/MyTrace" samples=<ask the user> variables=["PRG_Main.bEnable", ...]
trace_run app="Device/Plc Logic/Application" trace=... # downloads and starts
... do things (write_var, plc_start, ...) ...
trace_save trace=... # stops if needed, exports, returns the value changes per variableRepeat runs of the same trace: trace_run ... download=False re-arms the trigger and starts the
trace already on the controller, with no download. trace_stop stops it early. Download again
whenever the trace's variables or settings change.
The trace download is not an application download. It sends only the trace configuration to the
controller; the PLC program is untouched and the controller keeps running. On trace download the IDE
shows a dialog that the user may have to acknowledge. trace_run waits for it, so tell the user to
watch the IDE.
trace_setup also puts the variables in the trace's first diagram; without that they are recorded
but not drawn. Variable names are relative to the application: no Application. prefix. With the prefix the
download silently does nothing. A trace stops itself once its buffer has filled; trace_save copes with that. It returns a table of the points where any value
changed (time_ms plus one column per variable); sampling follows the task cycle.
Ladder and other graphical code (PLCopenXML)
get_pou and set_pou handle text only: the declaration of any POU, and the body of an ST POU.
create_pou can make an ld/fbd POU, but its body cannot be set from text (set_pou says so).
Graphical code goes in as PLCopenXML:
The ladder tools use two Claude Code skills on the Linux side, installed side by side in
~/.claude/skills (or set CTRLX_SKILLS_DIR): ctrlx-engineer (st_to_plcopenxml.py, the
converter) and codesys-lad (the ladder compiler and ld_trace.py). Claude passes the source
text to the tool; the server runs the skill's scripts on it. Nothing is copied to the Windows PC.
ladder_check sources={"FB_X.st": "<text>"}compiles without touching the IDE and returns the converter's warnings plustrace: every output as a boolean expression, per network. Check it against the intent.sourcesmaps a.stfile name to its text (a GVL is named after its file);libsholds declarations of library POUs the rungs call. A source error raises, with the network and the position.Ask the user, then
logout(the IDE refuses to add objects while logged in; the controller keeps running), andexport_plcopenthe objects you are about to replace as a backup.ladder_importwith the same arguments compiles and imports in one step; nothing is imported if the source does not compile. The XML goes through the tunnel as text and the bridge imports it from a temp file on the Windows side, so no shared folder is needed.conflictisReplace,CopyorSkipfor objects that already exist; confirmReplacewith the user.build, fix errors,loginas the user directs.
To review ladder that is already in the project, ladder_export paths=[...] returns the same
trace for it. include_xml=True adds the raw PLCopenXML. import_plcopen and export_plcopen
move raw XML for anything you built another way.
How the bridge works (and why)
These were found by trial; they explain the odd structure of bridge/bridge.py.
UI thread.
--runscriptruns the script on the IDE's UI thread, and a script that never returns freezes the IDE (a message pump in the loop did not help). So the script returns at once; a background thread accepts connections and runs each request on the UI thread through a hidden WinForms control (Control.Invoke). Building or logging in from the background thread fails withAn item with the same key has already been added(compiler).Stack empty. The online module keeps a stack of running scripts and raisesStack emptywhen it is empty, which it is once the bridge script has returned. The bridge saves the stack entry at startup and pushes it back before online calls.CTRLX_MCP_BLOCK=1serves on the script thread and never returns. Use it only for headless--noUIruns; the IDE window is frozen while it runs.Long calls (download, build) block the IDE UI while they run.
Stale online object. The bridge caches one online object per application. After a login or logout in the IDE it can go stale: reads fail with
Invalid variable referenceand writes do nothing.read_varandwrite_varretry once with a fresh object.Imports go where they are called.
import_xmlon the project puts new objects at the project root; on the application it puts them in the application.import_plcopencalls it onparent(default the application). Objects that already exist are matched and replaced either way.buildisapplication.build(), not a project method. Build messages come fromsystem.get_message_categories(True); do not callsystem.clear_messages()(it left the Messages pane broken). Compile problems can also surface only atlogin.Do not force-kill the IDE after a hung call; close it normally.
Tests
.venv/bin/python tests/test_protocol.py # bridge protocol, stubbed scriptengine
.venv/bin/python tests/test_mcp_e2e.py # MCP server against a stubbed bridgeThese do not run against a real IDE. What has been run for real: ping, tree, POU read/write,
save_project, build, login with download, logout, run_state, plc_start, read_var, write_var, and
the trace flow above, and PLCopenXML export/import (export_xml, and import_xml with text written
to a temp file) including a ladder block, driven through exec_script. Not exercised yet: the
export_plcopen, import_plcopen and ladder_* tools end to end, create_pou/create_dut/create_gvl/delete_object,
plc_stop, trigger-based traces, and starting the bridge with --runscript at IDE startup
(AUTORUN=1; the menu route is what has been tested).
License
MIT, see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
- platform7nOAuthtech.p7n
Connect Claude to your Platform7n workspaces: chat, links, work and decks. One-click OAuth.
Work on your own computers from Claude or ChatGPT: run commands, edit and search files.
Manage your IoT device fleet directly from Claude. Create device templates with datastreams and events, provision new devices, read live sensor data, and control outputs. The Blynk connector integrates with the Blynk IoT platform, enabling direct configuration and monitoring of connected devices and infrastructure.
Build, test and publish Olbrain AI agents from Claude Code, Codex or Claude.ai.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA Model Context Protocol server that enables seamless interaction between MCP clients (like Claude Desktop) and CODESYS V3 programming environments, allowing automation of project management, POU creation, code editing, and compilation tasks.47 npm126MIT
- AlicenseNot gradedqualityDmaintenanceEnables interaction with Siemens TIA Portal projects directly from VS Code or Claude Desktop using GitHub Copilot and the TIA Openness API. It supports connecting to TIA Portal instances, exporting blocks or types, and performing project operations like compilation via natural language.62MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to interact with B\&R Automation Studio projects by building code, running ARsim simulators, and reading or writing OPC UA variables. It facilitates industrial automation development and real-time variable integration through natural language commands.15MIT
- AlicenseAqualityFmaintenanceEnables Claude to read, analyze, optimize, and execute IP-Symcon PHP scripts via JSON-RPC API, turning automation development into a dialogue.13MIT