Persistent mode now uses a LazyPersistentExecutor everywhere the launcher
is not running: --no-auto-launch before first use, after shutdown_codesys,
and after a launch conflict. The first tool call launches the VISIBLE IDE
and delegates; nothing silently spawns --noUI processes anymore.
Why: headless spawns pop modal dialogs nobody can see (tool calls just
abort), hold .project locks (stale .~u files), and leave orphaned
CODESYS.exe processes that then block the next launch. Diagnosed live on
2026-06-11 while deploying TestN2k_v2_Fable: open_project/download ran
headless via the --no-auto-launch fallback and burned ~30 min on invisible
dialogs, zombie PIDs and lock-file cleanup.
Headless execution now requires explicit opt-in: --mode headless or
--fallback-headless.
Also lands the task-configuration tools from the 2026-06-09 session that
were complete but uncommitted: list_tasks, add_pou_to_task,
remove_pou_from_task (+ their IronPython scripts).
registerIdeBridgeTools() spawned CodesysMCPBridge.exe but the SIGINT/SIGTERM shutdown handler never closed the client, so every orchestrator exit left the shim running. They piled up across sessions (7 observed live with no IDE open).
- ide-bridge: add findOrphanedBridgePids()/killOrphanedBridges() using dead-parent detection (only reaps shims whose parent process is gone, so live sessions are untouched); harden close() with a taskkill /F fallback; expose .pid getter.
- server: sweep orphaned shims at startup, track the bridge client and close() it on shutdown, and add a process.on('exit') taskkill safety net for non-signal exits (stdin EOF / fatal).
Verified end-to-end against compiled dist: real orphan detected+reaped, live-parent shim left alone. Build clean, 156 tests pass. Bump 0.10.1 -> 0.10.2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CODESYS opens/saves projects from UNC paths (\server\share\...) only
unreliably -- it tends to fail late and opaquely. Add src/path-guard.ts
(isUncPath + uncPathError) and gate open_project, create_project,
save_project, and launch_codesys_with_project on it: each now returns
isError early telling the user to map a drive (net use Z: \server\share)
or copy the project to a local drive. Mapped drive letters and local
drives (incl. \?\C:\) are not treated as UNC, so existing workflows are
unaffected.
Also bumps version to 0.10.1 (first published release of the 0.10.x line;
includes the prior unreleased phobiCS-tui removal and add_device tool).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The phobiCS-tui CLI/UI is retired in favour of driving the codesys-mcp-sp21-plus
MCP tools directly from Claude Code (see https://docs.anthropic.com/claude-code).
The MCP server itself is unchanged; only the TUI front-end goes.
Removed:
- src/tui/ (browser + approve + shared + entry; 14 files)
- dist/tui/ (compiled output)
- tests/tui/ (14 .test.* files + the mini-mirror fixture tree)
- tsconfig.tui.json
- 3 superpowers plans/specs docs (2026-04-28 phobics-tui v0.1-v0.2, 2026-04-29 v0.3-live-values, 2026-04-28 tui-design)
- package.json: phobiCS-tui bin entry, build:tui script, TUI compile step in build, TUI typecheck step
- package.json: dependencies ink + react + diff (TUI-only); devDependencies @types/diff + @types/react + ink-testing-library
- README.md: ## phobiCS-tui section + ### Inline live values subsection
Git history side:
- Worktree .worktrees/phobics-tui removed (was on feature/phobics-tui-followup @ 7e427e9)
- Local + origin branches deleted:
- feature/phobics-tui (was 08ee361, 0 unmerged vs origin/main)
- feature/phobics-tui-followup (was 7e427e9, 0 unmerged vs origin/main)
- feature/phobics-tui-v0.3-live-values (was 9f4dc48, 0 unmerged vs origin/main)
- All three branches were merged into main, so deleting refs loses no history --
the commits remain reachable through main.
Knock-on (deliberately deferred):
- src/approve-gate.ts and the --approve-edits flag in src/bin.ts / src/server.ts
still exist. With the TUI gone, the gate auto-approves at every prompt and
prints a `[approve-gate] No TTY available -- phobiCS-tui cannot render` warning
to stderr. The infrastructure also still has callers in the uncommitted
add_device work, so a clean rip-out is left for a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Once launch() refused due to a foreign CODESYS, the error string was
cached in lastError and getStatus() returned that frozen snapshot
forever. Closing the foreign CODESYS did not update the status -- only
an MCP restart cleared it. From the user's POV: "every time CODESYS
HAS been open, you get a problem".
Fix: getStatus() now calls revalidateLaunchRefusal() first. If the
launcher is parked in 'error' state with a "Refusing to launch:"
prefix and findConflictingInstances() now returns empty, we transition
back to 'stopped' and clear lastError so the next status call / launch
attempt sees a fresh state. Other 'error' states (process died,
watcher timeout, exe not found) are not auto-cleared -- only the
launch-refusal cache, since that's the one that goes stale on its own
when the user closes the foreign window.
Bump 0.9.13 -> 0.9.14.
Tests: 2 new launcher unit tests pin the auto-clear behaviour and
verify unrelated 'error' states are NOT auto-cleared. 22/22 launcher
tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps ScriptObject.update(device_id) per the CODESYS Forge snippet so
a project can be retargeted between device families without destroying
the Application/POU/library subtree. Used for the WAGO PFC200 ->
CODESYS Control for Raspberry Pi MC SL workflow documented in the new
deploying-plc-project-to-rpi skill in mr-engineering-skill.
Intentionally NOT a remove+add fallback: if update() raises (cross-
family swap the IDE can't do in-place), surface the error rather than
silently destroying the subtree the user wants to keep.
Inputs:
projectFilePath -- target .project
targetDeviceName -- substring of repo display name
devicePath -- optional: device under project root; auto-picks
the first routed device, then any top-level
device, when omitted
targetVersion -- optional: exact version (else latest)
Detached spawn of an arbitrary CODESYS.exe with a .project as CLI arg
and optional --Profile= override. Useful when you want an SP22-saved
project opened in an SP21 IDE for SIM/inspection work, or when this
MCP is bound to install A but you want install B to handle the open
without registering a second server.
The launched IDE is not managed by this MCP: no IPC, no watcher, no
shutdown_codesys. Validates the exe + project paths up front and
returns the new PID.
Two new MCP tools to drive previously IDE-only workflows:
- add_device_user: add (or update password of) a user in the PLC
runtime's live User Management via
ScriptOnlineDevice.create_live_user_management() + add_user(). Required
for OPC UA authentication on CODESYS Control SP16+, where the OPC UA
server reads its UserIdentityToken policies from the runtime UserMgr.
Handles add-vs-update by calling live_um.upload() to sync local cache,
and falls back to set_user_password if add_user reports "already
existing" (the upload() race).
- grant_object_access: set Access Control permissions on a project
object for a user group. Maps to the IDE's Properties -> Access
Control matrix. Wraps ScriptUserManagement.get_object_permission() +
ScriptObjectPermission.set_permission_state(). Common use case: grant
Everyone View+Modify on the Symbol Configuration so the downloaded
OPC UA server can advertise UserIdentityTokens for clients.
Both tools follow the existing pattern: JSON markers in the script,
extractMarkerJson() to surface the output, ensure_project_open helper.
Also bumps grant_object_access timeout to 120s after observing that
first-touch project ACL operations can be slow.
ensure_project_open opened projects with VersionUpdateFlags.NoUpdates |
SilentMode. SilentMode loads the project into the scriptengine context
only -- the IDE's project explorer stays empty, so the user can't see
which project the MCP is operating on (or whether it actually loaded).
download_to_device, set_pou_code, etc. all still worked, but the IDE
window looked blank, which was confusing.
Drop SilentMode. NoUpdates stays (don't auto-update libraries on open;
that would silently change project content). With this change, opening
a project via the MCP behaves like a normal File -> Open: the tree
populates, the user can navigate it, and we can still drive
modifications from script.
When the MCP server runs under Claude Code (stdio JSON-RPC), there is
no TTY attached -- the parent process owns stdin/stdout for the protocol
channel. phobiCS-tui spawned with stdio=inherit then has nothing to
render to and exits immediately, which gateOpForTool was interpreting
as "user rejected the change" -- silently blocking every modifying
operation (download_to_device, set_pou_code, create_*, etc.) with no
visible cause.
Detect non-TTY at gate entry. When phobiCS-tui isn't viable, log a
clear warning to stderr (visible in MCP server logs) and let the
operation proceed instead of falsely rejecting it. Applies to both
the generic gateOpForTool path and the set_pou_code runApproveGate
path.
User can pass --no-approve-edits to silence the warning when they
explicitly don't want the gate. The right long-term fix is to route
approval through the MCP elicitation protocol so the user gets the
prompt in their chat UI; that's a separate change.
End-to-end download via MCP was failing for three reasons; all three
fixed here so the agent can drive the whole flow without manual IDE
clicks.
1. verify_device_reachable did a live gateway.perform_network_scan()
on every call. That freezes the CODESYS UI thread for the full
scan duration, so the user couldn't interact with the IDE during
pre-flight (and a 60s tool timeout was easy to hit). Now reads
gateway.get_cached_network_scan_result() first (instant, no UI
block), only falls back to a live scan when no cache exists.
Adds scan_source ("cache" | "live") to the JSON so callers know
which path was taken.
2. rebind_device_to_scan short-circuited with "already-bound" when
the resolved address matched the cached one. But the IDE's
Select-Device + OK flow re-applies set_gateway_and_address even
when the address didn't change -- that's what refreshes the
device's scanned_* properties and re-establishes a session.
Skipping it left the binding stale even after a successful
rebind call. Always apply now.
3. ensure_online_connection created an online_application but never
opened a device session. login() then waited on (or popped a
dialog for) a session nobody had opened -- which is what the
user was doing manually as "right-click device -> Connect".
Adds _ensure_device_connected() helper that locates the project's
PLC device, creates a ScriptOnlineDevice, and calls connect() if
not already connected. Idempotent; best-effort (logs and falls
through on failure so login() can still try its own path).
scan_network_devices, verify_device_reachable, and
rebind_device_to_scan_result all emit JSON between marker pairs in their
script stdout. The tool handlers wrapped that output with
formatToolResponse which returns only the success line on success --
hiding the actual data so the agent had nothing to act on.
Adds extractMarkerJson() helper that pulls the block between the start
and end markers and pretty-prints it, then returns the JSON in the tool
response. Now the agent sees the full scan results / reachability
candidates / rebind outcome.
The scan_network_devices, verify_device_reachable, and
rebind_device_to_scan templates used plain "..." strings for
PROJECT_FILE_PATH. With a UNC path like \\files\... the rendered Python
source had "\\files\..." which Python's source parser collapses to
"\files\..." (one backslash). ensure_project_open then saw the mangled
path as different from the (correctly-opened) primary, CLOSED the
primary project, and failed to reopen the corrupted path.
Other scripts use r"..." to bypass source-level escaping. Applying the
same to the three new ones.
Also bumps to 0.9.5.
* fix: disable headless fallback by default
Persistent mode silently fell back to running CODESYS with --noUI
whenever the launch refused (e.g. another GUI instance was already
running). That left the user staring at no window while the MCP
spawned hidden processes per command. Flip the default of
--fallback-headless to false so the MCP errors out loudly instead;
opt in explicitly if silent --noUI is actually wanted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: clear stale lastError on launch retry and successful ready
get_codesys_status kept reporting historical "Last Error: Refusing
to launch..." messages forever because lastError was set on failure
but never reset. Reset to null at the top of launch() so a retry
starts clean, and again when the watcher signals ready so a
successful re-launch wipes prior errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* release: v0.9.2 -- no headless fallback by default + clear stale lastError
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Karstein Phobic Nyvold Kvistad <karstein.kvistad@maritimerobotics.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The TUI subpackage imports 'diff' at runtime (computeHunks in
src/tui/shared/diff.ts), so a global install via 'npm install -g'
was breaking with:
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'diff'
imported from .../dist/tui/shared/diff.js
devDependencies are not installed by 'npm install -g <pkg>', only
'dependencies' are. Move 'diff' to the right section.
This bug shipped silently in 0.5.0 onwards because the source-build
flow during dev install everything, masking it. First reproduces
when a user does a fresh global install -- which is the only flow
the published package should support.
Bumps to 0.9.1; tag + republish required to fix the live install.
Bumps codesys-mcp-sp21-plus from 0.8.0 to 0.9.0.
Headline changes since v0.8.0:
Live values:
- Pump now descends one level into user-defined types. A var
whose declared type resolves to another mirror .st file (a
POU/FB/STRUCT/DUT) gets each of its members read as
<var>.<member>. Caller-supplied resolveTypeMirror dep so the
pump stays decoupled from the mirror layout.
- --live-values-interval <ms> CLI flag (default 500, clamped
[100, 60000]).
add_library:
- SP22 librarymanager attribute name fix. The SP22 stub
documents 'library_manager' (underscore) but the actual
injected attribute is 'librarymanager' (one word). Now
probes both with SP-version-aware preference + adds an
SP22-specific find_library dispatcher that walks
lm.repositories when the documented signature is rejected.
Bumps codesys-mcp-sp21-plus from 0.7.1 to 0.8.0 and phobiCS-tui's
--version output from v0.2.0 to v0.3.0.
Headline changes since v0.7.1:
TUI:
- Viewer overlays each declared variable's live runtime value
inline next to its declaration when the runtime is online.
- Viewer joins multi-line (* ... *) comments across line
boundaries (lines fully inside a block no longer get
false-highlighted keywords).
MCP server:
- new --live-values CLI flag starts a 500 ms pump that reads
runtime values for the variables of the user's current POU
selection and writes them to tui-live-values.json next to the
existing tui-state.json.
- --approve-edits now also gates 4 runtime tools:
write_variable, download_to_device, start_stop_application,
restart_runtime_ssh (in addition to the 9 modifying project
tools landed in v0.7.0).
No tag created here; npm publish has to be run from your terminal
because the npm 2FA passkey can't be driven through the bash tool.
Two bug fixes since v0.7.0:
e948922 fix(add_library): wrapper message reflects dedup vs add branch
d01f6ed fix(test): retry on PermissionError in mock_watcher (Win race)
Bumps codesys-mcp-sp21-plus to 0.7.0 (main was at 0.6.4 from
intermediate releases) and phobiCS-tui's --version output to v0.2.0.
Headline changes since v0.6.4:
TUI:
- browser keybinds: / (filter), o (open in editor), d (cross-
device diff), r (rescan), ? (help overlay)
- approve mode: v toggles unified <-> side-by-side diff
- viewer: ST syntax highlighting (keywords/types/comments/strings)
- statusbar: stale-mirror indicator + small-terminal resize warn
MCP server:
- --approve-edits now gates ALL 9 modifying tools, not just
set_pou_code: create_pou, create_property, create_method,
create_dut, create_gvl, create_folder, delete_object,
rename_object, add_library
No tag created here; npm publish has to be run from your terminal
because the npm 2FA passkey can't be driven through the bash tool.
The 0.6.3 fix replaced 'first-non-empty-pattern-wins' with category
iteration but used a HARDCODED list of 'well-known V3.5 GUIDs' that was
wrong. None of those GUIDs matched the actual Build category, so
compile errors stayed invisible (compile_project still reported '0
errors' even when the IDE-side download path saw them).
Diagnosed via a one-shot probe injected into compile_project on
2026-04-29: dumped attrs of script_engine.system, then called
script_engine.system.get_message_categories() (the METHOD) directly.
That returned the actual 7 category GUIDs in this CODESYS V3.5 SP22
Patch 1 install:
05581bd1-66d3-4251-aff2-047cc8e9adf7 Offline Help
936e1a33-3af8-47fa-b40b-903f0ae0b6cc Application Composer
a9b26e07-6ae1-4c06-9cd1-9ddddd397a2d SVN
0a6fcb64-7f24-43c6-a6d3-f70cb5d31114 (no parameterless ctor; Git)
194b48a9-ab51-43ae-b9a9-51d3edaaddf3 Script Messages
97f48d64-a2a3-4856-b640-75c046e37ea9 Build <-- the one we needed
220493a1-f49b-4416-9a3f-a545db707cbe Additional code checks
Real fix: replace the hardcoded list in _enumerate_categories() in both
compile_project.py and get_compile_messages.py with a call to
system.get_message_categories(); label each one via
get_message_category_description(guid). Iterate per category as before.
GUIDs are now discovered at runtime so the same code works on any SP
and any locale.
Verification: injected `THIS_IS_NOT_VALID_IEC_KEYWORD;` into
PLC_PRG.implementation, ran compile_project; output now reads
"1 error(s), 2 warning(s). ERROR: Identifier
'THIS_IS_NOT_VALID_IEC_KEYWORD' not defined". Restored PLC_PRG;
0 error(s) again. End-to-end fix confirmed.
Bumped 0.6.3 -> 0.6.4.
Two real bugs surfaced today:
1. compile_project / get_compile_messages reported 0 errors even when
the IDE's download path saw real compile errors. Root cause: both
scripts queried get_message_objects() with no category arg, which
returns only the IDE's last-active message tab (typically "Other"
for the WATCHER startup messages). Build/Code-Generation errors
live in a different category and were never queried.
Fix: enumerate script_engine.system.message_categories AND probe a
set of well-known V3.5 category GUIDs (Compile, Build, Online,
LibMan); query target_app.get_message_objects(cat) and
system.get_message_objects(cat) per category; aggregate dedup'd
entries (severity, text, object, line). Each entry now carries its
originating category label.
2. add_library's post-add _is_resolved() check trusted is_placeholder
== False as proof of resolution. CODESYS lets you call
add_placeholder(name_str) for a name that is NOT in the installed
Library Repository -- the resulting reference reports
is_placeholder=False yet has empty effective_version and the IDE
shows it with a yellow-warning triangle in Library Manager (no
Effective Version column populated). Karstein hit this with
"OPC UA PubSub SL": list_project_libraries reported it as
[managed] but compile failed because the IDE couldn't resolve it.
Fix: for the non-placeholder branch, probe effective_version /
resolved_version / version / resolved_library / managed_library /
library. ALL must be empty/None for the ref to be considered
hollow. Logs a DEBUG with the ref's attribute list so the next
such bug is diagnosable without source spelunking.
Bumped 0.6.2 -> 0.6.3.
Yesterday's 0.6.1 fix refused on every miss from _resolve_in_repo,
including the case where the IDE-level library_manager global is not
injected into the script context (which is the actual situation when
running through the MCP's script execution channel rather than the
interactive script REPL). Result: false-negative refuses for libraries
that ARE installed, just not visible from this script context.
This commit factors out _resolve_in_repo_accessible() and uses it to
distinguish three outcomes:
(a) Found in repo -> proceed; managed reference
(b) Repo accessible, name NOT hit -> HARD REFUSE (the bricking case)
Opt-in via ALLOW_UNRESOLVED=1.
(c) Repo NOT accessible at all -> proceed; rely on the post-add
_is_resolved() guard at line ~309
to catch hollow placeholders.
The (c) path is the safe relaxation: we cannot prove the library is
missing, so we defer to the post-add check rather than refuse blindly.
Adds for installed libraries succeed normally, broken adds still get
caught and removed before save.
Verified live: add_library 'OPC UA PubSub SL' against MCPTest2.project
(library was installed via Tools > Library Repository) now succeeds
cleanly with reference 'OPC UA PubSub SL [managed] ns=OPC_UA_PubSub_SL'.
The pre-resolve via library_manager.find_library() at line 218 already
detected when the requested name was not present in the installed
library repository -- it logged "Pre-resolve... returned no hit" -- but
the script then proceeded to call add_placeholder(LIBRARY_NAME) without
a managed-lib argument anyway. CODESYS happily creates such a reference
with is_placeholder=False, so the post-add _is_resolved() guard returns
True and the project gets saved with a hollow reference. The next time
the project is opened, the IDE pops:
Library Manager: Error: Could not open library 'X'.
(Reason: The placeholder library 'X' could not be resolved.)
...and compile fails until the user manually deletes the bad reference
from the Library Manager.
Karstein hit this on 2026-04-28 trying to add "OPC UA Pub Sub" (the
real library is "OPC UA PubSub SL", an add-on SL package not present
in the stock V3.5 SP22 install). The script returned SCRIPT_SUCCESS,
list_project_libraries showed it as `[managed]`, and only on the next
set_pou_code call did the broken-placeholder error surface.
Fix: when _resolve_in_repo returns None, hard-refuse upfront with a
clear error pointing at the Library Repository / CODESYS Installer.
Opt-in via ALLOW_UNRESOLVED=1 (mapped to the new MCP arg
`allowUnresolved: true`) for the rare case where a placeholder for a
not-yet-installed library is genuinely wanted.
Tool description and arg docs updated to mark allowUnresolved as
DANGEROUS so future agent calls don't reach for it casually.
Bumped 0.6.0 -> 0.6.1.
WHY: connect_to_device and download_to_device against a password-protected
runtime pop a modal "Device User Login" dialog in the IDE. IronPython can't
marshal to the WPF UI thread to dismiss it, so headless / agent-driven
sessions block forever -- and even for interactive use, the dialog pops on
EVERY download, which is a constant friction point.
API: ScriptOnline.set_default_credentials(username, password) was added in
CODESYS scripting API 3.5.3.0. Effect lasts until end of the current script
execution. Source: https://content.helpme-codesys.com/en/ScriptingEngine/ScriptOnline.html
Implementation:
- New helper script src/scripts/register_device_credentials.py defines a
register_device_credentials_if_set() function that no-ops when DEVICE_USER
or DEVICE_PASSWORD is empty, gracefully skips on older SPs that lack
set_default_credentials, and never raises (always falls back to the
current dialog-prompting behaviour).
- connect_to_device.py and download_to_device.py call the helper as the
FIRST action inside their try blocks, before ensure_project_open and
any login() attempt, so credentials are registered before any code path
that could trigger the dialog.
- server.ts adds optional deviceUser / devicePassword args to both tools'
input schemas. Resolution order:
args.deviceUser (per-call override)
-> process.env.CODESYS_DEVICE_USER
-> '' (empty, dialog pops as before)
Same for devicePassword. Env-var path is the recommended config:
claude mcp add -s user codesys-sp22-patch1 \
-e CODESYS_DEVICE_USER=Karstein \
-e CODESYS_DEVICE_PASSWORD=codesys123 \
-- codesys-mcp-sp21-plus --codesys-path ... --codesys-profile ... \
--mode persistent --no-auto-launch
Backward compat: when both creds are empty (default for existing users),
the helper short-circuits and behaviour is byte-identical to 0.5.x. No
regression. Verified by smoke-testing prepareScriptWithHelpers locally
with both filled and empty inputs -- function definition + callsite are
both wired in either case; only set_default_credentials() is suppressed
when empty.
README updated for connect_to_device and download_to_device tool rows.
Version bumped 0.5.0 -> 0.6.0.
WHY: an unlicensed CODESYS Control runtime drops out of demo mode every 2
hours. systemctl is-active reports "active" even after the binary has
died, so a TCP probe on the runtime port (default 11740) is the only
honest liveness signal. The new tool gives MCP a one-call path to bring
the runtime back without dropping into a terminal.
Implementation choices:
- ssh2 (npm) instead of spawning ssh/sshpass: sshpass is not on the
default Windows path, and the target Pi's sshd 10.x rejects pubkey
signatures from this client environment in practice. ssh2 handles
password auth + remote stdin + exit-code capture cross-platform.
- sudo -S with the password fed on remote stdin -- avoids a NOPASSWD
sudoers entry on the PLC.
- After issuing the restart, polls 'ss -tln | grep :<port>' once per
second until the listen port is up or livenessWaitSeconds expires.
This is what catches a half-dead runtime that systemctl reports as
fine.
Defaults match the only Pi we currently target (codesys-pi.local /
karstein / codesys123 / codesyscontrol / port 11740) but every field
is overridable.
Smoke-tested against codesys-pi.local: restart exit 0, port back up
after ~3s.
Per https://docs.npmjs.com/cli/v11/using-npm/scripts, since npm@7 the
preinstall/install/postinstall scripts run in the background with stdout
AND stderr captured. The only way to surface them is the
`--foreground-scripts` opt-in flag (default false). Three failed
publish cycles (0.4.4, 0.4.5, 0.4.6) couldn't get around this -- the
limitation is by design, not a bug.
Removing src/postinstall.ts and the postinstall script entry from
package.json. The functionality (printing the .mcp.json snippet) is
still available -- it just runs on demand via:
codesys-mcp-sp21-plus --print-config
Updated the README Quick Start to a numbered 4-step flow:
1. npm install -g codesys-mcp-sp21-plus
2. codesys-mcp-sp21-plus --print-config
3. paste into project- or user-scoped .mcp.json
4. restart Claude Code
Plus a footnote explaining why no banner -- so a future maintainer
doesn't try to re-add postinstall and waste another publish cycle.
Runs after `npm install -g codesys-mcp-sp21-plus`. Detects every
CODESYS install on PATH and prints the ready-to-paste .mcp.json
block per install (same output as `--print-config`).
Guards:
- Skipped during local installs / dev clones (npm_config_global != true)
- Skipped in CI (CI=true or npm_config_ci=true)
- Wrapped in try/catch + 'node ... || true' so a banner failure never
blocks the install
- Non-Windows: prints a note and exits cleanly
- Zero CODESYS installs: prints a hint pointing at --print-config
Resolves the awkward 'now run these two commands to verify and get
your config' step from the README.
Project rename. The 'SP21+' identifier is more accurate than the
previous 'SP22+' label -- this fork specifically carries the SP21+
migration fixes (the upstream's system.execute_on_primary_thread()
removal and downstream API drift), and is forward-compat with later
SPs. The repo on GitHub has been renamed to
phobicdotno/Codesys-MCP-SP21-plus accordingly.
Substitutions (UTF-8 preserved this time -- prior PowerShell pass
mangled em-dashes via a Win-1252 round-trip):
- package.json: name codesys-mcp-sp22-plus -> codesys-mcp-sp21-plus,
bin entry, repository.url, homepage, description, author trailer
- README.md: title, banner, install + clone snippets, CLI invocations
- ARCHITECTURE.md: comparison-table column header + temp-dir prefix
- tests/TEST_OVERVIEW.md: title
- src/bin.ts: header comment + program().name()
- src/launcher.ts: SESSION_DIR_PREFIX
- src/types.ts: header comment
Compatibility phrase 'Works on SP19, SP21, and SP22+' and the technical
identifier 'SP22 Patch 1 fixes' (which name the actual CODESYS version)
were preserved -- those refer to CODESYS, not the project name.
README additions:
- Quick Start example switched from SP21 Patch 3 to SP22 Patch 1 to
match the more common modern install
- New 'Multiple CODESYS installations' subsection with worked
side-by-side example for SP21 (3.5.21.50) and SP22 (3.5.22.10) --
one named MCP server entry per install, called by name from Claude.
Documents the constraint that --codesys-path/--codesys-profile are
bound at server startup, that --detect lists installs, and that
config edits require a Claude Code restart
- Tool count corrected from 28 -> 37 in the Features bullets
- New Tools sections covering 10 previously-undocumented tools:
Version Anchor + Release Pipeline (bump_project_version,
release_project_version, read_running_version_online), Source
Mirror (mirror_export), and CODESYS Git PDE-gated (git_init,
git_status, git_commit, git_remote_add, git_branch_set_upstream_to,
git_push). Each row carries the actual operational gotchas
discovered during this session (UNC localRepoPath rejection,
push-without-upstream failure, optimizer stripping unreferenced
globals from the online symbol table, etc.)
- list_project_libraries + add_library row text updated to reflect
the post-fix behaviour (compiler-version capture; managed-overload
preference + resolution gate)
Local origin URL updated: phobicdotno/Codesys-MCP -> phobicdotno/Codesys-MCP-SP21-plus.
37/37 tests green.
Project identity rename. Concrete changes:
- package.json
- name: codesys-mcp-persistent -> codesys-mcp-sp22-plus (npm-safe;
'+' is not allowed in npm package names)
- description: prepended 'Codesys-MCP-SP22+' branding + brief
summary of the SP22+ fixes carried in this fork
- bin entry: codesys-mcp-persistent -> codesys-mcp-sp22-plus
- repository.url + homepage: now point at
github.com/phobicdotno/Codesys-MCP-SP22-plus (using -plus
spelling on the URL side; '+' would require URL-encoding)
- author: noted upstream + SP22+ fork attribution
- README.md: title + about-this-fork blurb + clone instructions
- tests/TEST_OVERVIEW.md: title
- ARCHITECTURE.md: comparison-table column header + temp-dir path
- src/bin.ts: file header + program().name()
- src/launcher.ts: SESSION_DIR_PREFIX (with note on backwards compat)
- src/types.ts: file header
Display name everywhere: 'Codesys-MCP-SP22+'.
npm package name: 'codesys-mcp-sp22-plus'.
Out of scope for this commit -- need user action:
- Local directory rename (C:/Users/karstein.kvistad/Codesys-MCP ->
...-SP22+) cannot be done while the persistent MCP is using dist/
from the current path; user can rename after shutting down VS Code.
- GitHub repo rename (phobicdotno/Codesys-MCP -> ...-SP22-plus or
...-SP22+) needs the github.com web UI; the package.json URLs
already point at the post-rename name so a 'git remote set-url
origin <new>' lines up after.
- MCP client config in .mcp.json or similar: the bin name changed
from 'codesys-mcp-persistent' to 'codesys-mcp-sp22-plus' -- update
any 'command' field that referenced the old name. (Local sessions
invoking node dist/bin.js directly are unaffected.)
37/37 tests still green after rename.
v0.3.0 returning-watcher architecture — background thread polls for commands
and marshals execution onto the CODESYS UI thread, keeping the IDE fully
responsive between operations. File-based IPC with atomic writes, async mutex
command serialization, headless fallback, and 35 passing tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>