0
0
Fork 0
An MCP server for Codesys
Find a file
Karstein Phobic Nyvold Kvistad 976ea05236 fix(launcher): only refuse double-spawn of the SAME --codesys-path
Previous guard refused any CODESYS.exe in tasklist regardless of which
exe path the launcher was configured for. This broke the multi-install
setup the README documents (codesys-21 + codesys-22 entries are
supposed to coexist), and refused to launch any time the user had a
manual CODESYS window open from a different install.

Different CODESYS installs (e.g. SP21 + SP22) are designed to run in
parallel -- they're separate processes, separate IPC, separate file
locks. The only genuine conflict is two instances of the SAME exe
trying to attach to the SAME .project file (CODESYS pops 'project is
currently in use'). The same-exe case can't share IPC with us anyway
since we didn't spawn it.

Implementation:
- New findRunningCodesys() returns [{pid, exePath}] via PowerShell
  Get-Process (tasklist doesn't expose ExecutablePath; WMIC is
  deprecated on modern Windows).
- pathsEqual() exported helper: case-insensitive, slash-normalised,
  trims trailing separators.
- Spawn-guard now filters by pathsEqual(p.exePath, config.codesysPath).
  Refusal message names the conflicting exe and PIDs explicitly.
- shutdown_codesys orphan-killer also filters by exe path so we never
  kill a CODESYS instance the user owns or that belongs to a different
  MCP entry.

Tests:
- 6 new pathsEqual cases (identical / case-insensitive / slash-mix /
  trailing-sep / different installs / different drives).
- detect test for the new --print-config caveat copy (no longer
  warns 'only one at a time'; warns about same-.project conflict).
- 58/58 pass.

Also updates --print-config CAVEAT in src/detect.ts to reflect that
multiple entries can be active simultaneously, with the only hard rule
being don't open the same .project from two CODESYS instances.
2026-04-27 20:57:59 +02:00
.github/workflows Initial release: MCP server for CODESYS with persistent UI instance 2026-02-08 18:59:16 +10:00
docs fix: drop dead sp21-plus-migration-notes branch refs from generated md + smoke-test doc 2026-04-26 22:56:43 +02:00
src fix(launcher): only refuse double-spawn of the SAME --codesys-path 2026-04-27 20:57:59 +02:00
tests fix(launcher): only refuse double-spawn of the SAME --codesys-path 2026-04-27 20:57:59 +02:00
.gitignore Initial release: MCP server for CODESYS with persistent UI instance 2026-02-08 18:59:16 +10:00
ARCHITECTURE.md rename: Codesys-MCP-SP22+ -> Codesys-MCP-SP21+ + multi-install README + missing tools 2026-04-26 22:02:46 +02:00
LICENSE Initial release: MCP server for CODESYS with persistent UI instance 2026-02-08 18:59:16 +10:00
package-lock.json release: v0.4.7 -- drop broken postinstall, document the on-demand --print-config flow 2026-04-27 20:46:46 +02:00
package.json release: v0.4.7 -- drop broken postinstall, document the on-demand --print-config flow 2026-04-27 20:46:46 +02:00
README.md docs(README): drop the no-postinstall-banner footnote 2026-04-27 20:51:34 +02:00
tsconfig.json Initial release: MCP server for CODESYS with persistent UI instance 2026-02-08 18:59:16 +10:00
vitest.config.ts Initial release: MCP server for CODESYS with persistent UI instance 2026-02-08 18:59:16 +10:00

Codesys-MCP-SP21+

This is a fork. It is not the upstream luke-harriman/Codesys-MCP.

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 same Marshal error: The functionality 'system.execute_on_primary_thread(...)' is no longer supported and the server was effectively unusable on current CODESYS releases. Several other upstream tools were also broken by unrelated script-engine API drift. This fork fixes all of that and adds a release pipeline on top.

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

2. 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 --print-config           # one entry per detected install
codesys-mcp-sp21-plus --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 --print-config` on 2026-04-27.
// Detected 2 CODESYS installations. Add the entries you want; remove the rest.
//
// CAVEAT: the launcher refuses to spawn alongside any other CODESYS.exe,
// so only ONE of these can be active at a time. ...

{
  "mcpServers": {
    "codesys-sp21-patch5": {
      "command": "codesys-mcp-sp21-plus",
      "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 the mcpServers entries into the existing object if it does.
  • User-scoped (applies to every Claude Code session): %USERPROFILE%/.claude.json, or use claude mcp add codesys codesys-mcp-sp21-plus -- --codesys-path ... --codesys-profile ....

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.


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 in docs/MIGRATION-SP21-PLUS.md.
  • Cancel-link hardening. The watcher now catches KeyboardInterrupt (which is not a subclass of Exception in 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 passed name= as a kwarg the API doesn't accept; fixed to use positional foldername= with an SV_POU fallback for SP21+, then walks children to detect success since the API returns void.
  • compile_project / get_compile_messages — upstream choked on Python long values that json.dumps can't serialize on IronPython 2.7. Coerced to int before dumping.
  • connect_to_device — upstream used the wrong LoginMode signature. 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 via library_manager.find_library and 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 the ScriptLibManObjectContainer API (the previous one no longer exists), and now also captures IDE version, devices, and per-Application compiler version.

New tools (not in upstream)

  • mirror_export — walks the project tree and writes one .st file 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-part Project Information.Version (major / minor / revision / build / auto) and maintains a _MCP_PROJECT_VERSION GVL inside the project so the running PLC carries its source version. auto mode classifies via mirror diff vs the latest v* git tag (deletion/rename → major; addition → minor; modification → revision; first-run → seed at 1.0.0.0). Auto-maintains Changelog.md alongside 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 add controlled paths → git commitgit 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.sVersion from 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.
  • git_remote_add, git_branch_set_upstream_to, git_push — wrappers for the CODESYS Git plugin's remote/upstream/push primitives that upstream didn't ship. git_branch_set_upstream_to is mandatory before the first push to a fresh remote — without it git_push fails with "branch does not track an upstream branch".

Reliability fixes

  • launcher refuses to spawn when a CODESYS.exe is already running, instead of racing into a "project is currently in use" modal.
  • shutdown_codesys kills orphan CODESYS.exe when the launcher has no tracked PID (e.g. after a crashed parent).
  • git_* tools now project.save() after every mutating op so binding/state actually persists.

Verification

The verified state of every tool is recorded in docs/SMOKE-TEST-2026-04-25.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. It is this fork — not the upstream luke-harriman/Codesys-MCP 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. The CODESYS Git plugin tools (git_init, git_commit, git_push, etc.) additionally require an active CODESYS Professional Developer Edition subscription license.

npm install -g codesys-mcp-sp21-plus

That single command:

  • downloads the published tarball from https://www.npmjs.com/package/codesys-mcp-sp21-plus
  • installs it globally (-g) so the codesys-mcp-sp21-plus binary is on your PATH (typically %APPDATA%\npm\ on Windows)
  • pulls in its 4 dependencies (@modelcontextprotocol/sdk, commander, uuid, zod) automatically

Verify the install:

codesys-mcp-sp21-plus --version
codesys-mcp-sp21-plus --detect       # lists installed CODESYS versions

Then wire it into .mcp.json per Quick Start and start Claude Code.

To upgrade later:

npm install -g codesys-mcp-sp21-plus@latest

Install from source (development / unreleased changes)

If you want to track the fork's main branch directly, contribute fixes, or pin to a specific commit instead of the published version:

git clone https://github.com/phobicdotno/Codesys-MCP-SP21-plus.git
cd Codesys-MCP-SP21-plus
npm install
npm run build
npm link

npm link registers dist/bin.js as the global codesys-mcp-sp21-plus 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 \
  --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",
      "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",
      "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 under C:\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 --detect once to print every CODESYS install the server can see, with its profile name; copy the values from there into .mcp.json rather than guessing.
  • Each server entry spawns its own CODESYS process when first invoked. Don't call launch_codesys on both at the same time pointing at projects that overlap — two CODESYS instances racing on the same .project file 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.json at startup).

CLI Reference

Flag Description Default
-p, --codesys-path <path> Path to CODESYS executable $CODESYS_PATH or auto-detected
-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 if persistent fails true
--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>
--name <name> With --print-config --sp <n>: override the MCP server entry name
--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.

MCP Tools

37 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)
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

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)
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)

Online / Runtime Tools

Tool Description
connect_to_device Login to the PLC runtime — LoginMode signature + auto-login FIXED
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)
start_stop_application Start or stop the PLC application

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)

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 addgit commitgit 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.

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. Foundation for the release pipeline classifier

CODESYS Git (PDE license-gated)

These tools wrap CODESYS's own Git plugin. All of them require an active CODESYS Professional Developer Edition subscription license — without it, the runtime's HasGitLicense rule fails fast with a clear PDE-required message. Distinct from the orchestration-level git operations baked into release_project_version (which use the system git binary and don't need PDE).

Tool Description
git_init Initialise a Git working tree via project.git.init(). Dual-storage model: the .project stays where it is; the git repo lives in a SEPARATE directory (auto-defaults to <basename>_git sibling). Pass an explicit localRepoPath on a local drive when the project lives on a network share — UNC paths are rejected by the plugin
git_status Reports current branch + a probe of any status/changes/diff methods exposed on project.git. Read-only
git_commit Stages all working-tree changes and commits via project.git.commit_complete(message, user, mail)
git_remote_add NEW — Adds a named remote via project.git.remote_add(name, url)
git_branch_set_upstream_to NEW — Sets the current branch's upstream tracking ref. Mandatory before the first push to a fresh remote
git_push NEW — Pushes the current branch via project.git.push(). If username + token are both supplied, uses the 3-arg overload; otherwise relies on cached credentials / Windows Credential Manager. Security: when a token is supplied, it is briefly resident in the watcher's command file on disk — prefer cached credentials.

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

Execution Modes

Persistent Mode (default, SP21+ rewrite)

  1. Server launches CODESYS.exe with --runscript=watcher.py (no --noUI)
  2. CODESYS UI opens — user can see and interact with the IDE
  3. The watcher runs single-threaded on the primary thread, polling a commands/ directory and yielding to the IDE via system.delay() between polls (this fork — upstream used a background thread + system.execute_on_primary_thread() which was removed in SP21)
  4. When a tool is called, the server writes a .py script + .command.json to commands/
  5. The watcher detects the command, executes it directly on the primary thread, and writes results atomically to results/
  6. Changes made by tools appear in the CODESYS UI in real-time
  7. The UI remains interactive between commands — only briefly paused during synchronous API calls (compile, open)

Headless Mode

Falls back to the original approach: each tool call spawns a new CODESYS process with --noUI, runs the script, and exits. No UI is shown. Used when:

  • --mode headless is specified
  • Persistent mode fails to launch and --fallback-headless is enabled
  • CODESYS is launched with --no-auto-launch and launch_codesys hasn't been called yet

Detect Installed Versions

codesys-mcp-sp21-plus --detect

Scans 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 --verbose for 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:watch

Project Structure

src/
  bin.ts              CLI entry point
  server.ts           MCP tool/resource registration (37 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)
  integration/        Integration tests (script pipeline, manual CODESYS tests)
  mock_watcher.py     Standalone watcher for testing without CODESYS

Credits

  • Upstream project: luke-harriman/Codesys-MCP — original architecture, the persistent-watcher concept, and the bulk of the upstream tool set
  • This fork: phobicdotno/Codesys-MCP-SP21-plus — Karstein Kvistad. SP21+/SP22 watcher rewrite, upstream-tool fixes, version-anchor + release pipeline, source-mirror export, CODESYS Git push wrappers

License

MIT