codesys-mcp-sp21-plus-ch
v0.13.2
Published
Codesys-MCP-SP21+ (Chinese-friendly fork) -- downstream of phobicdotno/Codesys-MCP-SP21-plus adding byte-exact UTF-8 / Chinese round-trip for POU declarations, implementations, comments and the get_all_pou_code bulk-read path in both persistent and headle
Maintainers
Readme
Codesys-MCP-SP21+
This is a Chinese-friendly downstream fork. It is not the original
luke-harriman/Codesys-MCPand it is not thephobicdotno/Codesys-MCP-SP21-plusit forks from — it's a separate npm package with extra POU-text UTF-8 hardening on top.
- Original upstream: luke-harriman/Codesys-MCP — the original architecture and tool set.
- Direct upstream: phobicdotno/Codesys-MCP-SP21-plus — the SP21+/SP22 watcher rewrite + release pipeline this fork inherits.
- This fork: Limhslog/Codesys-MCP-SP21-plus-ch
- npm:
codesys-mcp-sp21-plus-ch— separate package; does not clash with the upstreamcodesys-mcp-sp21-plusif both are installed (the binary is namedcodesys-mcp-sp21-plus-ch).- Maintainer: Limhslog (this
-chfork).Why fork. Upstream's watcher relies on
system.execute_on_primary_thread()to marshal work from a background thread back to the CODESYS UI thread. That API was removed in CODESYS V3.5 SP21+, so on SP21 / SP22 every tool call returned the sameMarshal error: The functionality 'system.execute_on_primary_thread(...)' is no longer supportedand the server was effectively unusable on current CODESYS releases. Several other upstream tools were also broken by unrelated script-engine API drift. The direct upstream (phobicdotno/Codesys-MCP-SP21-plus) fixes all of that and adds a release pipeline on top.Why this further fork. The
-chsuffix stands for "Chinese": even after the direct upstream'sbase64(UTF-8)POU transport landed, several read paths and corner cases weren't pinned with regression tests, leaving silent mojibake risks for projects with Chinese comments /°C/ 中文字符. This fork adds the missing test coverage on both modes (persistent + headless), the support matrix in Chinese / Unicode support, and theparseAllPouCodeOutput/parsePouCodeOutputextraction that makes the contract testable.
MCP server for CODESYS with a persistent UI instance and file-based IPC. Unlike headless-only approaches that spawn a new CODESYS process per command, this server launches CODESYS with its UI visible and keeps it running. MCP tool calls are sent to the same instance via a file-based IPC watcher, so changes appear in real-time and the user can interact with the IDE alongside AI-driven automation.
Quick Start
1. Install globally from npm:
npm install -g codesys-mcp-sp21-plus-ch2. Generate your .mcp.json snippet — --print-config scans your installed CODESYS versions and emits a ready-to-paste block per install:
codesys-mcp-sp21-plus-ch --print-config # one entry per detected install
codesys-mcp-sp21-plus-ch --print-config --sp 21 # only the SP21 entry, named "codesys"Output looks like this on a machine with two installs:
// Auto-generated by `codesys-mcp-sp21-plus-ch --print-config` on 2026-04-27.
// Detected 2 CODESYS installations. Add the entries you want; remove the rest.
//
// Multiple entries can be active at the same time -- different CODESYS
// installs (e.g. SP21 + SP22) spawn separate processes and run side by side.
// The only hard rule: don't open the SAME .project file from two CODESYS
// instances simultaneously -- file-lock contention pops a "project is
// currently in use" modal that blocks all script execution.
{
"mcpServers": {
"codesys-sp21-patch5": {
"command": "codesys-mcp-sp21-plus-ch",
"args": [
"--codesys-path", "C:\\Program Files\\CODESYS 3.5.21.50\\CODESYS\\Common\\CODESYS.exe",
"--codesys-profile", "CODESYS V3.5 SP21 Patch 5",
"--mode", "persistent"
]
},
"codesys-sp22-patch1": { ... }
}
}3. Paste into your MCP config:
- Project-scoped (recommended, shareable via git):
<your-project-root>/.mcp.json. Create it if it doesn't exist; merge themcpServersentries into the existing object if it does. - User-scoped (applies to every Claude Code session):
%USERPROFILE%/.claude.json, or useclaude mcp add codesys codesys-mcp-sp21-plus-ch -- --codesys-path ... --codesys-profile .... - Using OpenAI Codex instead of Claude Code? It's the same stdio server — see Use with OpenAI Codex CLI for the
config.tomlequivalent.
The snippet is JSON with // comment headers — strip the comments before parsing if your tooling is strict.
4. Restart Claude Code so it re-reads the MCP config.
See Installation for source-install / upgrade / multi-install setups.
Use with OpenAI Codex CLI
codesys-mcp-sp21-plus-ch is a standard stdio MCP server, so any MCP-capable client can drive it — not just Claude Code. OpenAI's Codex CLI stores MCP servers in a TOML file rather than a JSON .mcp.json.
Config file location:
- Global:
~/.codex/config.toml(Windows:%USERPROFILE%\.codex\config.toml) - Per-project (trusted projects only):
.codex/config.tomlin the repo root
Translate the --print-config output into a [mcp_servers.<name>] table. --print-config emits Claude Code's JSON, but the command and args are identical — only the wrapper format differs. Run codesys-mcp-sp21-plus-ch --detect first to get the exact path + profile string, then:
[mcp_servers.codesys-sp21]
command = "codesys-mcp-sp21-plus-ch"
args = [
"--codesys-path", "C:\\Program Files\\CODESYS 3.5.21.50\\CODESYS\\Common\\CODESYS.exe",
"--codesys-profile", "CODESYS V3.5 SP21 Patch 5",
"--mode", "persistent",
"--no-auto-launch",
]
# CODESYS's first launch / a full compile can take 60s+. Codex defaults are
# startup_timeout_sec = 10 and tool_timeout_sec = 60 — raise the per-tool one.
tool_timeout_sec = 180
# Optional: pre-register PLC credentials so the "Device User Login" modal is
# suppressed on connect_to_device / download_to_device.
[mcp_servers.codesys-sp21.env]
CODESYS_DEVICE_USER = "<user>"
CODESYS_DEVICE_PASSWORD = "<password>"For multiple CODESYS installs, add a second table ([mcp_servers.codesys-sp22]) with its own --codesys-path / --codesys-profile — the TOML equivalent of the multi-install .mcp.json block under Multiple CODESYS installations.
Or let Codex write the entry for you instead of editing the file by hand:
codex mcp add codesys-sp21 \
--env CODESYS_DEVICE_USER=<user> --env CODESYS_DEVICE_PASSWORD=<password> \
-- codesys-mcp-sp21-plus-ch \
--codesys-path "C:\Program Files\CODESYS 3.5.21.50\CODESYS\Common\CODESYS.exe" \
--codesys-profile "CODESYS V3.5 SP21 Patch 5" \
--mode persistent --no-auto-launchNotes:
- Differences from Claude Code's config: a TOML
[mcp_servers.<name>]table (note the underscore) instead of the JSONmcpServersobject;envis a TOML table (or inlineenv = { KEY = "val" }); there's no outer wrapper object. --no-auto-launchis recommended here too so CODESYS opens on the first tool call rather than when Codex spawns the server (otherwise the launch runs during startup and can exceedstartup_timeout_sec).- Restart Codex after editing
config.toml— the MCP client only reads it at startup. - Everything else is identical because it's the same binary: one
CODESYS.exeat a time, exact--codesys-profilestrings (copy from--detect),--for-project,--auto-mirror, etc.
Live source-control diff (--auto-mirror)
When --auto-mirror is added to the server args, every successful modifying tool call (set_pou_code, create_pou, rename_object, add_library, ...) is followed by an automatic mirror_export. The textual <projectDir>/mcp-mirror/ tree is refreshed on disk in lock-step with the binary .project, so an external editor watching the folder sees the change immediately. The first refresh on a given project also fires code --add <mirrorDir> once, which appends the mirror folder to your active VSCode window so the diff shows up in the Source Control panel.
Multiple projects in one folder: when two or more
.projectfiles share a parent dir, each project gets its own mirror at<projectDir>/<projectname>_mcp_mirror/instead (e.g.ProjectA.project->ProjectA_mcp_mirror/,ProjectB.project->ProjectB_mcp_mirror/) so the exports don't clobber each other. Existing setups with amcp-mirror/directory keep using it as-is, regardless of how many.projectsiblings are present, so v* tag history stays intact.
Enable it by adding the flag to the relevant entry's args in .mcp.json:
"args": [
"--codesys-path", "...",
"--codesys-profile", "...",
"--mode", "persistent",
"--auto-mirror"
]Recommended one-time setup so the Source Control panel has a baseline to diff against:
cd <projectDir>/mcp-mirror
git init && git add -A && git commit -m "baseline"After that, watch VSCode's Source Control panel as Claude edits — every tool call shows up as a real diff. The VSCode hook is best-effort: if the code CLI shim isn't on the standard path, the mirror still refreshes silently and the response carries an (auto-mirror: refreshed) hint instead of opening a window.
What's new in this fork
Compatibility fixes (the headline)
- SP21+/SP22 compatibility. The watcher was rewritten as single-threaded on the primary thread, yielding to the IDE via
system.delay(). No background thread, no marshaling. Works on SP19, SP21, and SP22+. Full rationale indocs/migration-sp21-plus.md. - Cancel-link hardening. The watcher now catches
KeyboardInterrupt(which is not a subclass ofExceptionin IronPython 2.7) at three layers, so clicking "Click here to CANCEL this operation" in CODESYS no longer pops the modal traceback dialog or kills the watcher.WATCHER_VERSION 0.4.2.
Upstream tool fixes
create_folder— upstream passedname=as a kwarg the API doesn't accept; fixed to use positionalfoldername=with anSV_POUfallback for SP21+, then walks children to detect success since the API returns void.compile_project/get_compile_messages— upstream choked on Pythonlongvalues thatjson.dumpscan't serialize on IronPython 2.7. Coerced tointbefore dumping.connect_to_device— upstream used the wrongLoginModesignature. Fixed, plus the online tools now auto-login if you haven't already, instead of silently returning empty results.ensure_project_open— fixed the cross-project switch path so opening a second project no longer leaves the watcher pinned to the first.set_pou_code— upstream wiped the other half of the POU when only declaration or only implementation was passed. Now an omitted field is left intact.add_library— pre-resolves vialibrary_manager.find_libraryand prefers the managed-library overload. Refuses to save if the resulting reference is an unresolvable placeholder, which would otherwise brick the next project open.list_project_libraries— switched to theScriptLibManObjectContainerAPI (the previous one no longer exists), and now also captures IDE version, devices, and per-Application compiler version.
New tools (not in upstream)
- SP21 full API coverage (v0.11.0–v0.12.0) — 46 tools across 5 phases closing the gap to the SP21 ScriptEngine API: online/runtime ops (reset, force/unforce, bulk read/write, boot application, source up/download, PLC file transfer), project lifecycle (PLCopenXML + native export/import, project archive, compiled library, project info, compiler version), application build actions, device parameters + IO-mapping CSV + task configuration, and project user management. Per-category tables under MCP Tools; plan + status in
docs/superpowers/plans/2026-06-12-sp21-api-coverage.md. SVN, Application Composer and Automation Server scripting are deliberately out of scope (license-gated / addon products). mirror_export— walks the project tree and writes one.stfile per code-bearing object into<projectDir>/mcp-mirror/, preserving the project tree. Read-only; foundation for source-controlled CODESYS projects.bump_project_version— bumps one part of the 4-partProject Information.Version(major / minor / revision / build / auto) and maintains a_MCP_PROJECT_VERSIONGVL inside the project so the running PLC carries its source version.automode classifies via mirror diff vs the latestv*git tag (deletion/rename → major; addition → minor; modification → revision; first-run → seed at 1.0.0.0). Auto-maintainsChangelog.mdalongside the bump.release_project_version— one-shot release pipeline:mirror_export→ classify →bump_project_version→ regenerate library.md/pou-dump.md/README.md/Changelog.md →git addcontrolled paths →git commit→git tag v<new>→git push --follow-tags. Tag annotation embeds dual SHAs (project-sha256 + mirror-sha256) so the binary-changed-without-source-diff case still gets a build-bump with provenance.read_running_version_online— reads_MCP_PROJECT_VERSION.sVersionfrom the running PLC over the CODESYS online protocol (port 11740 / gateway). Returns the live value plus a sanity check against the X.Y.Z.W shape.
Reliability fixes
launcherrefuses to spawn a 2nd instance of the same CODESYS install (would conflict on the project file lock). Different installs (SP21 + SP22) coexist fine. Filters by--codesys-path, not just by image name, so multi-install setups work.shutdown_codesyskills orphanCODESYS.exeof the configured install when the launcher has no tracked PID (e.g. after a crashed parent). Other installs are left alone.- Template interpolation hardening (v0.12.1) —
$-sequences in tool-arg values (IEC string literals like'$R$N') are no longer mangled by regex replacement; user-arbitrary values (passwords, comments, PLC paths, device parameter values) are escaped into Python string literals instead of being pasted raw intor"..."templates;find_object_by_pathaccepts dot-separated paths all the way through its final name check; the build cleansdist/scriptsso deleted templates don't ship in the npm tarball. - Unicode-safe POU code transport (v0.12.4) —
set_pou_code/get_pou_codemove declaration+implementation payloads as base64(UTF-8);get_all_pou_coderides over IronPython'sjson.dumps(..., ensure_ascii=True); watcher/headless script I/O is UTF-8. Chinese comments and non-ASCII IEC text round-trip byte-identical in persistent and headless modes; Python templates remain ASCII-only for IronPython compatibility. Per-surface coverage table, strict-guarantee statement, and the regression-test inventory that pins it are in Chinese / Unicode support below.
Chinese / Unicode support
Per-surface coverage. "Persistent" = the visible-UI watcher that the launcher starts; "Headless" = --mode headless, which spawns CODESYS.exe --noUI per command.
| Surface | Persistent (UI watcher) | Headless (--noUI) | Mechanism |
|---|:---:|:---:|---|
| set_pou_code declaration / implementation payload | ✅ | ✅ | TS encodes UTF-8 → base64; set_pou_code.py base64.b64decode → unicode → .replace() |
| get_pou_code declaration / implementation read | ✅ | ✅ | get_pou_code.py emits base64(UTF-8) between ### POU ... B64 ... ### markers; TS parsePouCodeOutput → fromBase64Utf8 |
| codesys://project/{...}/pou/{...}/code resource | ✅ | ✅ | Same base64-marker decoder |
| get_all_pou_code bulk read | ✅ | ✅ | json.dumps(..., ensure_ascii=True) → \uXXXX over stdout; JS JSON.parse decodes back |
| mirror_export .st files | ✅ | ✅ | codecs.open(..., 'utf-8'); read-only snapshot, no write-back |
| Live-values .st VAR parser | ✅ | ✅ | Strips // and (* *) (including Chinese inside) before identifier regex |
| Watcher command / script file I/O | ✅ | n/a | codecs.open(..., 'utf-8') for both command JSON and script .py |
| Headless temp script file write | n/a | ✅ | fs.writeFileSync(..., 'utf-8') |
| Object / POU / path identifiers | ❌ | ❌ | Pass ASCII identifiers only. The transport layer does not corrupt non-ASCII names, but IEC 61131-3 forbids them in production projects and several auxiliary tools (mirror_export filename sanitiser, git on cross-platform clones) make no Unicode-name guarantees. |
| mirror_export → project write-back (a.k.a. "mirror import") | n/a | n/a | Not implemented. mcp-mirror/ is a read-only export. |
The strict guarantee. set_pou_code({ declarationCode, implementationCode }) followed by get_pou_code({ pouPath }) returns the two strings byte-identical to the input, with full Chinese (Han) coverage. No NFC/NFD normalization is performed. The same guarantee holds in --mode headless. Round-trip via mcp-mirror/.st files is not part of this guarantee — mirror is one-way.
Regression tests pinning the contract: tests/integration/e2e.test.ts (set_pou_code -> get_pou_code chinese round-trip is byte-exact, and the bulk-read counterpart), tests/unit/headless.test.ts (headless stdout carrying base64 chinese markers decodes via parsePouCodeOutput), tests/unit/all-pou-code-parse.test.ts, tests/unit/ipc.test.ts (sendCommand preserves UTF-8 script content), tests/unit/var-block-parse.test.ts (Chinese-comment cases).
Verification
The verified state of every tool is recorded in docs/function-test-2026-04-25.md (and the 2026-04-28 re-verification in docs/function-test-2026-04-28.md). Open issues (mostly online-API drift) are tracked in docs/open-bugs-cross-reference.md.
Installation
This is a Node.js MCP server published to npm as codesys-mcp-sp21-plus-ch (note the -ch suffix — distinct from the upstream codesys-mcp-sp21-plus package). It is this Chinese-friendly downstream fork — not the original luke-harriman/Codesys-MCP, not the direct upstream phobicdotno/Codesys-MCP-SP21-plus, and not a Python package. There is no pip install; the .py files under src/scripts/ are CODESYS IronPython templates bundled inside the npm package itself.
Requirements: Node.js 18+, Windows, CODESYS 3.5 SP19, SP21 (3.5.21.x), or SP22 (3.5.22.x) installed.
Install from npm (recommended)
npm install -g codesys-mcp-sp21-plus-chThat single command:
- downloads the published tarball from https://www.npmjs.com/package/codesys-mcp-sp21-plus-ch
- installs it globally (
-g) so thecodesys-mcp-sp21-plus-chbinary is on your PATH (typically%APPDATA%\npm\on Windows) - pulls in its 5 dependencies (
@modelcontextprotocol/sdk,commander,ssh2,uuid,zod) automatically
Verify the install:
codesys-mcp-sp21-plus-ch --version
codesys-mcp-sp21-plus-ch --detect # lists installed CODESYS versionsThen wire it into .mcp.json per Quick Start and start Claude Code.
To upgrade later:
npm install -g codesys-mcp-sp21-plus-ch@latestInstall from source (development / unreleased changes)
If you want to track this fork's main branch directly, contribute fixes, or pin to a specific commit instead of the published version:
git clone https://github.com/Limhslog/Codesys-MCP-SP21-plus-ch.git
cd Codesys-MCP-SP21-plus-ch
npm install
npm run build
npm linknpm link registers dist/bin.js as the global codesys-mcp-sp21-plus-ch binary, so the same .mcp.json snippet works. Edits to src/ take effect after npm run build; Python script edits hot-reload from dist/scripts/ without a rebuild.
To update later: git pull && npm install && npm run build.
If you'd rather avoid touching the global node_modules, skip npm link and reference the local checkout directly in .mcp.json:
{
"mcpServers": {
"codesys": {
"command": "node",
"args": [
"C:\\Users\\<you>\\Codesys-MCP-SP21-plus\\dist\\bin.js",
"--codesys-path", "C:\\Program Files\\CODESYS 3.5.22.10\\CODESYS\\Common\\CODESYS.exe",
"--codesys-profile", "CODESYS V3.5 SP22 Patch 1",
"--mode", "persistent"
]
}
}
}Run without .mcp.json
You can also invoke the binary directly from a shell (useful for one-off testing or wrapping in another launcher):
codesys-mcp-sp21-plus-ch \
--codesys-path "C:\Program Files\CODESYS 3.5.22.10\CODESYS\Common\CODESYS.exe" \
--codesys-profile "CODESYS V3.5 SP22 Patch 1"Multiple CODESYS installations
The MCP server is bound to a single --codesys-path / --codesys-profile at startup. launch_codesys takes no parameters — it just starts whichever CODESYS the server was configured against. If you have several CODESYS versions installed and want to drive them all from the same Claude Code session, register one MCP server entry per install with a distinct name.
Both blocks below live in the same .mcp.json. Claude can call either by name (codesys-21 / codesys-22) and the two run as independent processes with independent CODESYS instances:
{
"mcpServers": {
"codesys-21": {
"command": "codesys-mcp-sp21-plus-ch",
"args": [
"--codesys-path", "C:\\Program Files\\CODESYS 3.5.21.50\\CODESYS\\Common\\CODESYS.exe",
"--codesys-profile", "CODESYS V3.5 SP21 Patch 5",
"--mode", "persistent"
]
},
"codesys-22": {
"command": "codesys-mcp-sp21-plus-ch",
"args": [
"--codesys-path", "C:\\Program Files\\CODESYS 3.5.22.10\\CODESYS\\Common\\CODESYS.exe",
"--codesys-profile", "CODESYS V3.5 SP22 Patch 1",
"--mode", "persistent"
]
}
}
}Notes:
- The version numbers (
3.5.21.50,3.5.22.10) match the install directory names underC:\Program Files\— these are the actual install IDs CODESYS uses, not the marketing names. The marketing name lives in--codesys-profile(e.g.,CODESYS V3.5 SP21 Patch 5,CODESYS V3.5 SP22 Patch 1). - Run
codesys-mcp-sp21-plus-ch --detectonce to print every CODESYS install the server can see, with its profile name; copy the values from there into.mcp.jsonrather than guessing. - Each server entry spawns its own CODESYS process when first invoked. Don't call
launch_codesyson both at the same time pointing at projects that overlap — two CODESYS instances racing on the same.projectfile pop a "project is currently in use" modal that blocks every subsequent script. - Adding or removing an entry requires a Claude Code restart (the MCP client only reads
.mcp.jsonat startup).
If you have a specific .project file in mind and don't want to eyeball which install opens it, point --for-project at the file and --print-config will narrow the snippet to just the matching install (or warn and fall back to same-SP-different-patch if no exact match exists). The match is driven by the project's saved projectinspectiondata.auxiliary profile, so it works without launching CODESYS:
codesys-mcp-sp21-plus-ch --print-config --for-project "C:\path\to\MyMachine.project"CLI Reference
| Flag | Description | Default |
|------|-------------|---------|
| -p, --codesys-path <path> | Path to CODESYS executable | $CODESYS_PATH or C:\Program Files\CODESYS 3.5.21.0\CODESYS\Common\CODESYS.exe (hard-coded fallback, not auto-detected — run --detect and pass the result explicitly) |
| -f, --codesys-profile <name> | CODESYS profile name | $CODESYS_PROFILE or CODESYS V3.5 SP21 |
| -w, --workspace <dir> | Workspace directory for relative paths | Current directory |
| -m, --mode <mode> | persistent (UI) or headless (--noUI) | persistent |
| --no-auto-launch | Don't launch CODESYS on startup | Auto-launch enabled |
| --fallback-headless | Fall back to headless (--noUI) if persistent launch fails | false |
| --keep-alive | Keep CODESYS running after server stops | false |
| --timeout <ms> | Default command timeout | 60000 |
| --detect | List installed CODESYS versions and exit | — |
| --print-config | Print a ready-to-paste .mcp.json snippet for every detected install and exit | — |
| --sp <number> | With --print-config: emit only the entry for CODESYS V3.5 SP<n> | — |
| --for-project <path> | With --print-config: pick only the install(s) matching the .project file at <path> (exact SP+patch, or fall back to same-SP-different-patch). Mutually exclusive with --sp. | — |
| --name <name> | With --print-config --sp <n>: override the MCP server entry name | — |
| --inspect <path> | Read a CODESYS .project offline (no CODESYS needed) and print its profile name/version + mandatory libraries; uses the unzip CLI from Git for Windows / Linux+Mac | — |
| --ssh-version <host> | SSH to a CODESYS Control Linux PLC and print the running project version (extracted from the boot-application binary). Bypasses CODESYS entirely. Requires SSH key auth + passwordless sudo for strings. | — |
| --ssh-user <name> | With --ssh-version: SSH user | karstein |
| --ssh-boot-app <path> | With --ssh-version: path to the boot application on the PLC | /var/opt/codesys/PlcLogic/Application/Application.app |
| --verbose | Enable verbose logging | — |
| --debug | Enable debug logging | — |
| -V, --version | Show version number | — |
| -h, --help | Show help | — |
Environment variables CODESYS_PATH and CODESYS_PROFILE are used as defaults when the corresponding flags are not provided.
--ssh-version — read the running PLC's project version over SSH
For CODESYS Control Linux PLCs (Raspberry Pi, IPC, etc.) the running project version can be read straight off the boot-application binary, without CODESYS being installed or the .project file being unlocked:
codesys-mcp-sp21-plus-ch --ssh-version 192.168.1.83
codesys-mcp-sp21-plus-ch --ssh-version myplc.lan --ssh-user piRequires SSH key auth + passwordless sudo for /usr/bin/strings on the PLC. If your key isn't installed yet, the error message includes a one-line PowerShell recipe; full setup instructions live at ssh-key-windows.md.
MCP Tools
103 tools across the categories below. Tools marked NEW were added in this fork; tools marked FIXED existed upstream but were broken before this fork.
Management Tools
| Tool | Description |
|------|-------------|
| launch_codesys | Manually launch CODESYS (use with --no-auto-launch) |
| shutdown_codesys | Shut down the persistent CODESYS instance (kills orphans too) |
| get_codesys_status | Get current state, PID, execution mode |
Project Tools
| Tool | Description |
|------|-------------|
| open_project | Open an existing CODESYS project file (cross-project switch FIXED; SP-mismatch pre-flight NEW) |
| create_project | Create a new project from the standard template |
| save_project | Save the currently open project |
| compile_project | Build the primary application with structured error output (120s timeout) — JSON long FIXED |
| get_compile_messages | Retrieve last compiler messages without triggering a new build — JSON long FIXED |
open_project runs an offline pre-flight (projectinspectiondata.auxiliary ZIP+XML — no CODESYS) that compares the project's saved profile against this server's --codesys-profile. Exact match proceeds silently; same-SP-different-patch proceeds with a one-line warning (CODESYS will pop its patch-difference dialog); SP mismatch refuses without opening so the project isn't dragged through a downgrade/upgrade conversion. The refusal includes a routing hint: pick a different MCP server entry or generate one with codesys-mcp-sp21-plus-ch --print-config --for-project "<projectFilePath>". If the inspection itself fails (file missing, malformed .project, non-standard profile name), pre-flight falls through silently and the existing CODESYS open path produces its native error.
POU / Code Authoring Tools
| Tool | Description |
|------|-------------|
| create_pou | Create a Program, Function Block, or Function |
| set_pou_code | Set declaration and/or implementation code (omitted-field wipe FIXED; Unicode-safe — Chinese comments/text round-trip byte-exact, see Chinese / Unicode support) |
| create_property | Create a property within a Function Block |
| create_method | Create a method within a Function Block |
| create_dut | Create a Data Unit Type (Structure, Enumeration, Union, Alias) |
| create_gvl | Create a Global Variable List with optional initial declaration |
| create_folder | Create an organizational folder in the project tree (FIXED) |
| delete_object | Delete any project object (POU, DUT, GVL, folder, etc.) |
| rename_object | Rename any project object |
| get_all_pou_code | Bulk read all declaration and implementation code in the project (120s timeout; Unicode-safe via ensure_ascii=True JSON, Chinese content preserved) |
Online / Runtime Tools
| Tool | Description |
|------|-------------|
| connect_to_device | Login to the PLC runtime — LoginMode signature + auto-login FIXED; NEW deviceUser/devicePassword args (or CODESYS_DEVICE_USER/CODESYS_DEVICE_PASSWORD env) pre-register credentials via ScriptOnline.set_default_credentials so the modal "Device User Login" dialog is suppressed |
| disconnect_from_device | Logout from the PLC runtime |
| get_application_state | Check if the PLC application is running, stopped, or in exception |
| read_variable | Read a live variable value from the running PLC (e.g., PLC_PRG.bMotorRunning) |
| write_variable | Write/force a variable value on the running PLC |
| download_to_device | Download compiled application to PLC (attempts online change first, 120s timeout); same deviceUser/devicePassword credential-injection support as connect_to_device so the Device User Login dialog can be suppressed on every download too. Runs verify_device_reachable as a pre-flight |
| start_stop_application | Start or stop the PLC application |
| reset_application | NEW — Reset the online application: warm (keep retains), cold (clear retains), origin (erase application from device — destructive) |
| read_variables | NEW — Bulk read: current values of many expressions in one call (read_values) |
| write_variables | NEW — Bulk write: stage many expression/value pairs, commit in one write_prepared_values batch (same-cycle landing) |
| force_variables | NEW — Force expression/value pairs (pinned against task writes until unforced) |
| unforce_variables | NEW — Unforce specific expressions (optionally restoring pre-force values) or ALL forced values |
| list_forced_variables | NEW — List all forced + prepared expressions on the online application |
| create_boot_application | NEW — Create a boot application on the connected device, or write an offline .app file |
| source_download | NEW — Store the project source archive on the PLC (compact or full) |
| source_upload | NEW — Retrieve the source archive stored on the PLC into a local .prj |
| plc_file_list | NEW — List a directory on the PLC filesystem (kind/name/size/mtime) |
| plc_file_transfer | NEW — Copy a single file to (to_plc) or from (from_plc) the PLC filesystem |
| plc_file_delete | NEW — Delete a file or directory on the PLC filesystem (destructive) |
Project Lifecycle & Interop (NEW, SP21-coverage phase 2)
| Tool | Description |
|------|-------------|
| close_project | Close the open project (optionally saving first) |
| save_project_as | Save under a new path; set/disable encryption password |
| save_project_archive | Save a .projectarchive with default categories |
| save_as_compiled_library | Save the primary project as a .compiled_library |
| export_plcopen_xml / import_plcopen_xml | PLCopenXML interchange (whole project or subtree) |
| export_native / import_native | Lossless CODESYS native export/import |
| get_project_info / set_project_info | Project Information fields + custom properties |
| get_compiler_version / set_compiler_version_to_newest | Project compiler version |
| clean_all | Clean All (remove compile info) |
Application Build & Object Ops (NEW, phase 3)
| Tool | Description |
|------|-------------|
| application_build | generate_code / rebuild / clean on the active application |
| check_online_change | Is an online change currently possible? |
| move_object | Move an object to a new parent in the tree |
| get_signature_crc | Signature CRC of a POU (API-compatibility checks) |
| set_exclude_from_build | Set/clear 'Exclude from build' on an object |
Device Config & Task Config (NEW, phase 4)
| Tool | Description |
|------|-------------|
| list_device_parameters / get_device_parameter / set_device_parameter | Walk and edit device + connector parameters |
| export_io_mappings_csv / import_io_mappings_csv | Bulk IO-mapping editing via CSV |
| set_device_state | enable / disable / simulation_on / simulation_off |
| get_device_identification | Device type/id/version, name, address, state |
| create_task / configure_task | Create tasks and set kind/priority/interval/event |
Project Users & Misc Objects (NEW, phase 5)
| Tool | Description |
|------|-------------|
| list_project_users / add_project_user / remove_project_user | Project access-protection user management |
| create_text_list / import_text_list_file | Text lists for visu texts/translations |
| create_image_pool | Image pools for visualizations |
| add_external_file | Embed/link an external file into the project |
| restart_runtime_ssh | NEW — SSH into a Linux PLC and restart codesyscontrol via password-fed sudo -S. After issuing systemctl restart, polls ss -tln for the runtime port (default 11740) until it actually comes up — works around systemctl is-active reporting "active" after the binary has died from license-demo expiry. Defaults match the codesys-pi.local Pi |
Device Network / Access Management (NEW)
The gateway's cached device address goes stale every time the PLC reboots or gets a new router entry; these tools scan and re-bind without hand-editing the project. The two access-control tools cover the OPC UA prerequisites: the runtime user database (consulted for UserIdentityToken) and the project-side Access Control matrix on the Symbol Configuration object.
| Tool | Description |
|------|-------------|
| scan_network_devices | NEW — Drive the gateway's Scan Network on the project's configured device. Returns the live target list (device_name, type_name, vendor_name, address, device_id). useCache=true returns the gateway's last result without rescanning |
| verify_device_reachable | NEW — Pre-flight for download_to_device / connect_to_device: scans and reports whether the project's cached address still matches a live target. download_to_device runs this automatically |
| rebind_device_to_scan_result | NEW — Re-bind the project's configured device to a fresh scan result (same PLC, new address after reboot/DHCP). Match priority: matchName (exact, case-insensitive) → matchDeviceId → matchAddress (forced, no scan) → single candidate. Refuses on ambiguity and returns the candidate list |
| add_device_user | NEW — Add (or update the password of) a user in the PLC runtime's live User Management. Required for OPC UA authentication on CODESYS Control SP16+ — without at least one user, UaExpert returns BadIdentityTokenInvalid. The OPC UA server reads its UserIdentityToken policies from this database, NOT from CODESYSControl.cfg |
| grant_object_access | NEW — Set Access Control permissions on a project object for a user group (mirrors Properties → Access Control in the IDE). Required before a downloaded OPC UA server exposes any token policies: if the group has no View/Modify on the Symbol Configuration, there's nothing to expose. Common usage: grant Everyone View+Modify on CodesysRpi/Plc Logic/Application/Symbols |
Library Management Tools
| Tool | Description |
|------|-------------|
| list_project_libraries | List all libraries referenced in the project with version info, plus IDE version, devices, and per-Application compiler version (FIXED — switched to ScriptLibManObjectContainer) |
| add_library | Add a library reference. Pre-resolves via library_manager.find_library and prefers the managed-library overload; refuses to save if the resulting reference is an unresolvable placeholder (hardened) |
| remove_library | NEW — Remove a library reference from Library Manager. Idempotent: no-op + success if the named library isn't present. Accepts a bare name ('Standard') or the fully-qualified 'Name, Version (Company)' form to target a specific version when duplicates exist. Verifies removal in lm.references before saving |
Symbol Configuration Tools (NEW)
Wraps ScriptSymbolConfigObject (CODESYS 3.5.10.0+). The Symbol Configuration object controls which IEC variables / FBs / methods are exposed to OPC UA, web visualisations, and other external clients. Reference: helpme-codesys.com/en/ScriptingEngine/ScriptSymbolConfigObject.html and the SP22 stub Stubs/scriptengine/ScriptSymbolConfigObject.pyi.
| Tool | Description |
|------|-------------|
| find_symbol_config | NEW — Locate the Symbol Configuration object(s) in the project tree (one per Application typically). Read-only |
| list_all_signatures | NEW — Every POU / FunctionBlock / Method / Function the symbol config could potentially export. compile=true forces an application.build() first |
| list_all_datatypes | NEW — Every DUT / struct / enum / alias / union (same compile semantics) |
| list_configured_symbols | NEW — Only those signatures + datatypes actually configured for export, with each variable's configured_access / maximal_access / effective_access |
| get_symbol_config_settings | NEW — Read every knob: content_feature_flags (OPC UA / IncludeComments / IncludeAttributes / IncludeExecutables / etc.), attribute filter, comment filter, direct I/O access (+ obstacles), client-side layout calculator |
| create_symbol_config | NEW — application.create_symbol_config(...) under a chosen Application. Idempotent: no-ops with success if a symbol config already exists anywhere in the tree |
| set_symbol_config_settings | NEW — Partial-update of any subset of the 6 knobs. Refuses to enable direct I/O if check_effective_direct_io_access() reports obstacles |
| set_symbol_access | NEW — Per-variable configured_access setter (None / ReadOnly / WriteOnly / ReadWrite). Locates the signature by FQN; works on not-yet-configured variables too |
| set_signature_access_bulk | NEW — Set every variable in one signature to the same access in one call |
| export_symbol_xsd | NEW — Write the schema bytes from get_symbol_configuration_xsd() to a file (UTF-8). Useful for downstream XML validation in CI |
Version Anchor + Release Pipeline (NEW)
These tools maintain a _MCP_PROJECT_VERSION GVL inside the project so the running PLC carries its source version at a known address, and orchestrate the end-to-end release flow (mirror → classify → bump → regen .md → git commit + tag + push).
| Tool | Description |
|------|-------------|
| bump_project_version | NEW — Bump one part of the 4-part Project Information.Version (major / minor / revision / build / auto) and maintain _MCP_PROJECT_VERSION.sVersion. auto mode classifies via mirror diff vs latest v* git tag |
| release_project_version | NEW — One-shot release pipeline: mirror_export → classify → bump_project_version → regenerate .md docs → git add → git commit → git tag v<new> → git push --follow-tags. Dual-SHA tag annotation |
| read_running_version_online | NEW — Reads _MCP_PROJECT_VERSION.sVersion from the running PLC over the CODESYS online protocol (port 11740 / gateway). Caveat: requires some IEC code to reference the variable so the optimizer doesn't strip it from the online symbol table — see the tool's error message for the one-line fix. |
| read_running_version_ssh | NEW — SSH equivalent of read_running_version_online: extracts the X.Y.Z.W literal of _MCP_PROJECT_VERSION.sVersion straight off the boot-application binary on a CODESYS Control Linux PLC. Bypasses CODESYS entirely — no IDE, no project lock, no online protocol. Requires SSH key auth + passwordless sudo for strings on the PLC. Same engine as the --ssh-version CLI flag |
Source Mirror (NEW)
| Tool | Description |
|------|-------------|
| mirror_export | NEW — Walks the project tree and writes one .st file per code-bearing object into <projectDir>/mcp-mirror/, preserving the project tree as nested directories. Read-only (no write-back). UTF-8 encoded — Chinese comments are readable in the exported .st files. Foundation for the release pipeline classifier |
MCP Resources
| Resource URI | Description |
|--------------|-------------|
| codesys://project/status | CODESYS scripting status and open project info |
| codesys://project/{path}/structure | Project tree structure |
| codesys://project/{path}/pou/{pou}/code | POU declaration and implementation code (Unicode-safe — base64(UTF-8) markers decoded server-side, Chinese content preserved byte-exact) |
Execution Modes
Persistent Mode (default, SP21+ rewrite)
- Server launches
CODESYS.exewith--runscript=watcher.py(no--noUI) - CODESYS UI opens — user can see and interact with the IDE
- The watcher runs single-threaded on the primary thread, polling a
commands/directory and yielding to the IDE viasystem.delay()between polls (this fork — upstream used a background thread +system.execute_on_primary_thread()which was removed in SP21) - When a tool is called, the server writes a
.pyscript +.command.jsontocommands/ - The watcher detects the command, executes it directly on the primary thread, and writes results atomically to
results/ - Changes made by tools appear in the CODESYS UI in real-time
- The UI remains interactive between commands — only briefly paused during synchronous API calls (compile, open)
Headless Mode
The original approach: each tool call spawns a new CODESYS process with --noUI, runs the script, and exits. No UI is shown. Used only when:
--mode headlessis specified, or- Persistent mode fails to launch and
--fallback-headlessis explicitly opted in (off by default)
Persistent mode never silently degrades to headless. With --no-auto-launch, the first tool call lazy-launches the visible IDE; after shutdown_codesys, the next tool call relaunches it. Headless spawns are avoided because their modal dialogs are invisible (calls just abort), they hold .project locks, and they leave orphaned CODESYS.exe processes behind.
Detect Installed Versions
codesys-mcp-sp21-plus-ch --detectScans Program Files and Program Files (x86) for CODESYS installations.
Troubleshooting
CODESYS not found
Verify the path with --detect. The executable is typically at:
C:\Program Files\CODESYS 3.5.XX.X\CODESYS\Common\CODESYS.exe
Project file locked
Another CODESYS instance may have the project open. Close it first or use persistent mode so there's only one instance. The launcher will refuse to spawn a second CODESYS.exe.
Watcher timeout (persistent mode) If the watcher doesn't signal ready within 60 seconds, check:
- CODESYS path and profile are correct
- No modal dialogs are blocking CODESYS startup
- Try
--verbosefor detailed logging
UI briefly pauses during commands (persistent mode) The watcher executes commands on the primary thread and yields between polls, so the UI stays responsive between commands. During synchronous CODESYS API calls (compile, project open), the UI may briefly pause — this is expected and normal. If a command hangs, check the CODESYS messages window for modal dialogs or errors.
Command timeout
Default is 60s (120s for compile and download). Increase with --timeout <ms>. Check CODESYS messages window for errors.
Online/runtime tools fail
The online tools (connect_to_device, read_variable, etc.) require:
- A device/gateway configured in the CODESYS project
- The project to be compiled successfully before connecting
- A reachable PLC or CODESYS SoftPLC runtime
Development
# Install dependencies
npm install
# Build (compiles TypeScript + copies Python scripts)
npm run build
# Run all tests
npm test
# Type check only
npm run typecheck
# Run tests in watch mode
npm run test:watchProject Structure
src/
bin.ts CLI entry point
server.ts MCP tool/resource registration (103 tools, 3 resources)
launcher.ts CODESYS process management
ipc.ts File-based IPC transport
headless.ts Headless fallback executor
script-manager.ts Python template loading + interpolation
types.ts Shared TypeScript types
logger.ts Structured stderr logging
scripts/ Python scripts (watcher + helpers + tool scripts)
tests/
unit/ Unit tests (IPC, script manager, launcher, var-block parse,
Chinese round-trip on get_all_pou_code, headless UTF-8 path)
integration/ Integration tests (script pipeline, Chinese set↔get round-trip,
manual CODESYS tests)
mock_watcher.py Standalone watcher for testing without CODESYSCredits
- Original upstream: luke-harriman/Codesys-MCP — original architecture, the persistent-watcher concept, and the bulk of the upstream tool set.
- Direct upstream: phobicdotno/Codesys-MCP-SP21-plus — Karstein Kvistad. SP21+/SP22 watcher rewrite, upstream-tool fixes, version-anchor + release pipeline, source-mirror export, base64(UTF-8) POU transport.
- This fork: Limhslog/Codesys-MCP-SP21-plus-ch — Limhslog. Per-surface Chinese / UTF-8 support matrix, byte-exact round-trip pinning tests for persistent and headless modes,
parseAllPouCodeOutput/parsePouCodeOutputextraction, defensiveensure_ascii=Trueon the bulk-read path.
License
MIT
