0
0
Fork 0
Codesys-MCP-SP21-plus/README.md
Karstein Phobic Nyvold Kvistad 6d5d9b0385 docs: README for v0.2 followup (TUI keybinds + 9-tool gating)
Updates the phobiCS-tui section to cover the new keybinds (/, o, d,
r, ?, v) and documents that --approve-edits now gates all 9
modifying MCP tools (create_pou, create_property, create_method,
create_dut, create_gvl, create_folder, delete_object, rename_object,
add_library) on top of the existing set_pou_code.

Also notes the Viewer syntax highlighting and the statusbar's
mirror-staleness indicator + small-terminal resize warning.
2026-04-29 08:22:09 +02:00

510 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Codesys-MCP-SP21+
> **This is a fork.** It is **not** the upstream `luke-harriman/Codesys-MCP`.
>
> - **Upstream:** [luke-harriman/Codesys-MCP](https://github.com/luke-harriman/Codesys-MCP)
> - **This fork:** [phobicdotno/Codesys-MCP-SP21-plus](https://github.com/phobicdotno/Codesys-MCP-SP21-plus)
> - **npm:** [`codesys-mcp-sp21-plus`](https://www.npmjs.com/package/codesys-mcp-sp21-plus) (published by `phobic`)
> - **Maintainer:** Karstein Kvistad
>
> **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:**
```bash
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:
```bash
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:
```jsonc
// 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.
//
// 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",
"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](#installation) for source-install / upgrade / multi-install setups.
---
## 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 `.project` files 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 a `mcp-mirror/` directory keep using it as-is, regardless of how many `.project` siblings are present, so v* tag history stays intact.
Enable it by adding the flag to the relevant entry's `args` in `.mcp.json`:
```jsonc
"args": [
"--codesys-path", "...",
"--codesys-profile", "...",
"--mode", "persistent",
"--auto-mirror"
]
```
Recommended one-time setup so the Source Control panel has a baseline to diff against:
```bash
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 in [`docs/migration-sp21-plus.md`](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 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.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.
### Reliability fixes
- **`launcher`** refuses 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_codesys`** kills orphan `CODESYS.exe` of the configured install when the launcher has no tracked PID (e.g. after a crashed parent). Other installs are left alone.
### Verification
The verified state of every tool is recorded in [`docs/function-test-2026-04-25.md`](docs/function-test-2026-04-25.md) (and the 2026-04-28 re-verification in [`docs/function-test-2026-04-28.md`](docs/function-test-2026-04-28.md)). Open issues (mostly online-API drift) are tracked in [`docs/open-bugs-cross-reference.md`](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.
### Install from npm (recommended)
```bash
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:
```bash
codesys-mcp-sp21-plus --version
codesys-mcp-sp21-plus --detect # lists installed CODESYS versions
```
Then wire it into `.mcp.json` per [Quick Start](#quick-start) and start Claude Code.
To upgrade later:
```bash
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:
```bash
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`:
```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):
```bash
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:
```json
{
"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).
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:
```bash
codesys-mcp-sp21-plus --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 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>` | — |
| `--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:
```bash
codesys-mcp-sp21-plus --ssh-version 192.168.1.83
codesys-mcp-sp21-plus --ssh-version myplc.lan --ssh-user pi
```
Requires 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](https://gitlab.usv.no/karstein.kvistad/mr-ai-context/-/blob/main/ssh-key-windows.md).
## phobiCS-tui
This package ships a small ink TUI for browsing CODESYS-exported ST. After installing, run:
phobiCS-tui # auto-discovers mcp-mirror/ from cwd
phobiCS-tui <projectDir> # explicit project directory
phobiCS-tui approve <a.st> <b.st> # diff prompt; exit 0 = accept, 1 = reject, 2 = error
Browser-mode keys: `j`/`k` (or `↓`/`↑`) move the cursor, `l`/`Enter`/`→` expand a device, `h`/`←` collapse, `/` filter POUs by name (Enter commits, Esc clears), `o` open the highlighted POU in `$EDITOR` (or VS Code), `d` diff against the same-named POU in another device, `r` re-scan `mcp-mirror/`, `?` toggle the help overlay, `q` quits.
Approve-mode keys: `y` accept, `n`/`q`/`Esc` reject, `v` toggle unified ↔ side-by-side diff.
The browser writes the current selection to `%LOCALAPPDATA%/codesys-mcp/tui-state.json` (Windows) or `$XDG_STATE_HOME/codesys-mcp/tui-state.json` (Linux/Mac, defaulting to `~/.local/state/...`). The MCP tool `get_user_selection` reads it so an agent can ground its actions in what the user is looking at. The header shows mirror staleness when the on-disk export is older than 10 s, and a yellow resize warning appears below 80×20.
The Viewer applies ST syntax highlighting (cyan keywords, magenta types, gray comments, yellow strings).
Approve mode is opt-in for the MCP server's modifying tools — start the server with `--approve-edits` to wire it in. The v0.2 followup gates **all 9 modifying tools**: `create_pou`, `create_property`, `create_method`, `create_dut`, `create_gvl`, `create_folder`, `delete_object`, `rename_object`, `add_library` — plus the original `set_pou_code`. Each operation pops a y/n diff prompt; create/delete render as all-green/all-red one-sided diffs, rename as a del+add of the leaf name, and `set_pou_code` as a real diff against the existing mirror file. Off by default.
## MCP Tools
41 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 --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**) |
| `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**; **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 |
| `start_stop_application` | Start or stop the PLC application |
| `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 |
### 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**) |
### 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.* |
### 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 |
## 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
```bash
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
```bash
# 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 (41 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](https://github.com/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](https://github.com/phobicdotno/Codesys-MCP-SP21-plus) — Karstein Kvistad. SP21+/SP22 watcher rewrite, upstream-tool fixes, version-anchor + release pipeline, source-mirror export
## License
MIT