MCP server that exposes the CODESYS V3.5 IDE scripting API as 41 tools and 3 resources, with the CODESYS UI kept open across calls via a file-based IPC watcher.
Unlike headless-only wrappers that spawn a new --noUI CODESYS process per command, this server launches CODESYS with its UI visible and routes every tool call through a watcher script running inside that same instance. Changes appear in the IDE in real time and the user can interact with CODESYS alongside AI-driven automation.
- Windows
- Node.js 18+
- CODESYS V3.5 SP19 or SP20 for persistent mode. SP21+ runs only in headless mode — see Known limitations.
git clone <repository-url>
cd Codesys-MCP
npm install
npm run build
npm link.mcp.json (Claude Code):
{
"mcpServers": {
"codesys": {
"command": "codesys-mcp-persistent",
"args": [
"--codesys-path", "C:\\Program Files\\CODESYS 3.5.20.0\\CODESYS\\Common\\CODESYS.exe",
"--codesys-profile", "CODESYS V3.5 SP20",
"--mode", "persistent"
]
}
}
}For SP21+, set --mode headless (each tool call spawns a --noUI CODESYS process).
| Flag | Description | Default |
|---|---|---|
-p, --codesys-path <path> |
Path to CODESYS.exe |
$CODESYS_PATH or C:\Program Files\CODESYS 3.5.21.0\CODESYS\Common\CODESYS.exe |
-f, --codesys-profile <name> |
CODESYS profile name | $CODESYS_PROFILE or CODESYS V3.5 SP21 |
-w, --workspace <dir> |
Workspace for relative paths | cwd |
-m, --mode <mode> |
persistent or headless |
persistent |
--no-auto-launch |
Don't launch CODESYS on startup | auto-launch on |
--fallback-headless |
Fall back to headless if persistent launch fails | true |
--keep-alive |
Keep CODESYS running after server stops | false |
--kill-existing-codesys |
Kill any running CODESYS.exe before launching |
false |
--timeout <ms> |
Default command timeout | 60000 |
--detect |
List installed CODESYS versions and exit | - |
--verbose / --debug |
Logging | - |
Environment variables:
CODESYS_PATH,CODESYS_PROFILE— defaults for the corresponding flagsCODESYS_MCP_READY_TIMEOUT_MS— watcher-ready deadline (default180000)
| Tool | Description |
|---|---|
launch_codesys |
Manually launch CODESYS (use with --no-auto-launch). Adopts a live session if one exists |
shutdown_codesys |
Shut down the persistent CODESYS instance |
get_codesys_status |
Report state, PID, execution mode |
eval_python |
[DEV] Execute arbitrary IronPython 2.7 against the live scriptengine. Code must print("SCRIPT_SUCCESS") before exit |
| Tool | Description |
|---|---|
open_project |
Open an existing .project file |
create_project |
Create a new project. Defaults to copying CODESYS's installed Standard.project; pass templatePath to copy a specific .project, or templateName to instantiate a template registered with CODESYS's Template Manager |
list_project_templates |
Enumerate templates from CODESYS's Template Manager and %ProgramData%/CODESYS |
save_project |
Save the currently open project |
compile_project |
Build with structured error output (120s timeout) |
get_compile_messages |
Retrieve last compiler messages without rebuilding |
| Tool | Description |
|---|---|
create_pou |
Create a Program, Function Block, or Function |
set_pou_code |
Set declaration and/or implementation. Accepts Method/Property paths (Application/MyFB/Method1). Auto-saves to disk |
create_property / create_method |
Add a Property / Method to a Function Block |
create_dut |
Create a Data Unit Type (Structure, Enumeration, Union, Alias) |
create_gvl |
Create a Global Variable List, optionally with initial declaration |
create_folder |
Create an organisational folder |
delete_object |
Delete a user-created object. Refuses system nodes (Application, Device, Library Manager, etc.) and top-level paths |
rename_object |
Rename any project object |
get_all_pou_code |
Bulk read every declaration + implementation in the project (120s timeout) |
search_code |
Regex or literal substring search across every textual POU/Method/Property/DUT/GVL body |
find_references |
Word-boundary search for a symbol; wraps search_code |
rename_symbol |
Textual rename across all POU bodies. Two-phase plan + apply; dryRun=true by default |
| Tool | Description |
|---|---|
connect_to_device |
Login to the PLC runtime. Optionally pass ipAddress (and gatewayName, default Gateway-1); the device address is set then re-resolved from IP form to the gateway-scan node form before login |
disconnect_from_device |
Logout. No-op if not connected |
set_credentials |
Set default username/password for subsequent logins. Username must be non-empty |
set_simulation_mode |
Toggle device-level simulation mode |
get_application_state |
Report run / stop / exception plus login status |
read_variable |
Read a live variable. Path: GVL.var / PRG.var / GVL.s.aRoi[0].field (no Application. prefix) |
write_variable |
Write via set_prepared_value + force_prepared_values. The variable is FORCED until unforced or runtime restart |
download_to_device |
Download compiled application. mode: auto (default), online_change, or full. Boot application created after login. 120s timeout |
start_stop_application |
Start or stop the PLC application |
monitor_variables |
Sample one or more variables at a fixed interval. Duration capped at 60s, intervalMs floor 10ms |
Online tools route every scriptengine.online call through an ExecuteSource frame via the with_executor helper in ensure_online_connection.py. Without it, create_online_application raises InvalidOperationException: Stack empty from IPC-driven scripts on real PLCs.
| Tool | Description |
|---|---|
list_project_libraries |
List referenced libraries with version info |
add_library |
Add a library reference. Pass the fully-qualified placeholder string from the Library Manager UI (e.g. Standard, * (System)) — bare names like Util won't resolve |
| Tool | Description |
|---|---|
list_device_repository |
Enumerate device descriptors in the local CODESYS Device Repository. Optional vendor, nameContains, maxResults filters |
inspect_device_node |
Read-only introspection of a device node: descriptor metadata, parameters with values, child sub-devices |
add_device |
Wrap parent.add_device(name, type, id, version). Pair with list_device_repository to source canonical ids |
set_device_parameter |
EXPERIMENTAL. Many fieldbus parameters are GUI-only and return a clear error |
map_io_channel |
Bind (or clear) a fieldbus I/O channel to a global variable. Channel resolved by name path (Inputs/Byte 0/Bit 3) or numeric indices (0/3) |
| Tool | Description |
|---|---|
create_project_archive |
Save the open project as a .projectarchive. Saves unsaved edits first |
| URI | Description |
|---|---|
codesys://project/status |
Scripting status and open-project info |
codesys://project/{path}/structure |
Project tree |
codesys://project/{path}/pou/{pou}/code |
POU / Method / Property declaration + implementation |
{path} and {pou} use RFC 6570 reserved expansion — pass values with raw : and /, not percent-encoded. On Windows, use forward slashes:
codesys://project/C:/Users/me/Documents/MyPLC.project/structure
codesys://project/C:/Users/me/Documents/MyPLC.project/pou/Application/MyFB/Method1/code
The two templated resources don't appear in
ListMcpResourcesTool(onlyproject-statusis static).
⚠️ If the requested project path differs from the currently-open project, the resource handler refuses to switch — it raises rather than silently swapping context.
Persistent (default). On launch, the server scans %TEMP%/codesys-mcp-persistent/ for a live session left by a previous MCP server (matching profile, PID alive, ready.signal present) and adopts it. Otherwise it spawns CODESYS.exe --runscript=watcher.py (no --noUI). The watcher writes ready.signal, then runs a background thread that polls a commands/ directory and marshals each script onto the CODESYS UI thread via system.execute_on_primary_thread(). Results land in results/; Node polls with exponential backoff (100ms → 1s). The UI stays interactive between commands and pauses briefly during synchronous API calls (compile, open).
Headless. Each tool call spawns a new --noUI CODESYS process, runs the script, and exits. No UI. Used when --mode headless is set, when --no-auto-launch is used and launch_codesys hasn't been called, or when persistent launch fails and --fallback-headless is on.
codesys-mcp-persistent --detectScans Program Files and Program Files (x86) for CODESYS installations.
- Persistent mode is broken on CODESYS V3.5 SP21+. CODESYS removed
se.system.execute_on_primary_thread()from the scripting API somewhere in the SP21 line. Persistent launch completes (ready.signalfires) and then every tool call returnsMarshal error: The functionality 'system.execute_on_primary_thread(...)' is no longer supported.--fallback-headlessis launch-time only and does not catch this. Workarounds: (a) run with--mode headless; (b) use the forward-port at phobicdotno/Codesys-MCP-SP21-plus. SP19/SP20 are unaffected. set_pou_codeauto-saves to disk every successful call. UI Ctrl+Z does NOT recover prior content.set_pou_codeempty string is a no-op. PassingdeclarationCode: ""orimplementationCode: ""leaves the section unchanged. To clear a section, pass a single-line placeholder (e.g.// cleared).delete_objectrefuses system nodes and any top-level object (no/). System-reserved exact paths:Application,Device,Project Settings,Device/Plc Logic,Application/Library Manager,Application/Task Configuration,Application/Task Configuration/MainTask,Device/Communication,Device/Communication/Ethernet,Device/SoftMotion General Axis Pool(and theDevice/Plc Logic/...variants).set_credentialsrejects empty usernames. For no-auth runtimes, do not call this tool at all.add_libraryrequires a fully-qualified placeholder matching the Library Manager UI string (e.g.Standard, * (System)). Bare names fail withplaceholder library X could not be resolved.- DUT names containing
.are rejected by CODESYS (The name 'X.Y' is not valid for this object). - Identifier length isn't enforced at create time. CODESYS only complains at compile/save. Stick to ≤32-char IEC identifiers.
find_object_by_pathrefuses ambiguous matches and returnsNone. Pass a more specific path.is_simulation_modegetter returnsNoneon some device descriptors. The setter works; verify via compile + login.
CODESYS not found — verify with --detect. Executable is typically at C:\Program Files\CODESYS 3.5.XX.X\CODESYS\Common\CODESYS.exe.
Every tool call returns Marshal error: ... is no longer supported — you're on SP21+. Restart with --mode headless.
Watcher timeout — default is 180s. Cold first launches of older SPs can exceed the older 60s budget. Bump further on slow hardware: CODESYS_MCP_READY_TIMEOUT_MS=300000. If CODESYS is still coming up, just call launch_codesys again — the launcher will re-attach to the live PID rather than spawning a second instance.
Command timeout — default 60s; compile_project, get_all_pou_code, and download_to_device use 120s. Increase with --timeout <ms>.
Project file locked across MCP restarts — the launcher adopts a live session from the prior server on startup. If you still hit StandardError: The selected project is currently in use, kill the orphan CODESYS.exe via Task Manager, or pass --kill-existing-codesys on next launch (off by default to protect external IDE sessions).
Online tools fail — they require a device/gateway configured (or connect_to_device(ipAddress=...), or set_simulation_mode(enable=true)), a successful compile, and a reachable PLC.
Network error: No route to host from connect_to_device even though ping works — V3 login routes by the gateway-scan node address (e.g. 0301.B0F7), not by the IP-encoded form (0192.0168.0083.0247) that set_gateway_and_address stores from a raw IP. connect_to_device re-scans and re-sets the address before login; if it still fails, the gateway-to-runtime port (TCP 11740 by default) is probably blocked.
npm install
npm run build # tsc + copy src/scripts/ -> dist/scripts/
npm test
npm run typecheck
pwsh dev/check-scripts.ps1 # ASCII + template-reference static checksProject layout:
src/
bin.ts CLI entry point
server.ts MCP tool / resource registration
launcher.ts CODESYS process management + session adoption
ipc.ts File-based IPC transport
headless.ts Headless fallback executor
script-manager.ts Python template loading + interpolation
executor-proxy.ts Race-free executor swap during background auto-launch
result-parser.ts RESULT_JSON marker extraction
scripts/ IronPython 2.7 watcher + helpers + tool scripts
tests/
unit/ IPC, script manager, launcher
integration/ Script pipeline + manual CODESYS tests
MIT