0
0
Fork 0
Commit graph

106 commits

Author SHA1 Message Date
Karstein Phobic Nyvold Kvistad
37fc80764e feat(ssh): read_running_version_ssh -- read PLC project version over SSH, no CODESYS needed
New MCP tool + --ssh-version CLI flag. Bypasses the CODESYS IDE
entirely: SSH to a CODESYS Control Linux PLC, sudo strings the boot
application binary, extract the X.Y.Z.W literal of
_MCP_PROJECT_VERSION.sVersion. Filters out 3.5.x.y CODESYS runtime
versions automatically.

Solves the case where the .project file is locked by another CODESYS
instance, or no CODESYS install is reachable, but the PLC is. Read-
only on the PLC (just strings the boot binary).

Requires SSH key auth + passwordless sudo for /usr/bin/strings on
the PLC. Both error paths surface exact-instructions error messages
(PowerShell key install command, sudoers line) instead of opaque
failures.

Smoke-tested against codesys-pi (RPi running CODESYS Control 3.5.22)
with MCPTest2 v1.5.0.0 downloaded -- correctly extracts 1.5.0.0 and
filters out the 3.5.22.0 runtime version literal.
2026-04-27 22:13:29 +02:00
Karstein Phobic Nyvold Kvistad
0fa9b3852b feat(auto-mirror): wrap 13 modifying tools so --auto-mirror actually works
The 75cf74d scaffold added the helpers; this commit makes them
load-bearing by switching every modifying tool's formatToolResponse
call to formatModifyingResponse. Without --auto-mirror, behaviour is
unchanged. With it: mirror_export runs after each successful edit and
'code --add <mirror>' fires once per project to surface the diff in
VSCode's Source Control panel.
2026-04-27 21:53:10 +02:00
Karstein Phobic Nyvold Kvistad
4f5c80e591 docs(README): sync stale text -- new launcher behaviour + remove dead CODESYS Git mentions
Two pieces of doc lag the source had silently outpaced:

1. The embedded --print-config sample showed the old caveat ('only ONE
   can be active at a time') even though the runtime output (in
   src/detect.ts) was already updated to the post-0.4.8 wording about
   multi-install coexistence. Sync the README sample.
2. The 'Reliability fixes' bullet for the launcher still described the
   pre-0.4.8 behaviour (refuses ANY CODESYS.exe). Updated to the
   path-filtered version. Also drop the git_* project.save() bullet --
   those tools were removed in 5be20a6.
2026-04-27 21:48:59 +02:00
Karstein Phobic Nyvold Kvistad
0afdc08c47 feat(cli): --inspect <project> reads CODESYS profile + mandatory libs offline (no CODESYS needed) 2026-04-27 21:47:31 +02:00
Karstein Phobic Nyvold Kvistad
5be20a6e3b remove(codesys-git): drop all 6 CODESYS Git plugin tools
The CODESYS Git plugin wrappers (git_init, git_status, git_commit,
git_remote_add, git_branch_set_upstream_to, git_push) operated on the
binary .project file via CODESYS's IDE-side Git plugin. Drawbacks:

- Required a CODESYS Professional Developer Edition subscription
  (HasGitLicense gate). Anyone without PDE got a fail-fast error on
  every call -- the tools were dead weight for most users.
- Operated on a separate dual-storage repo (the .project stayed put,
  the git repo lived in a sibling directory). Diffs were unreadable
  because they're binary serialisations, not source text.
- Couldn't run on UNC paths -- the plugin rejected them.
- Duplicated functionality release_project_version already provides
  via the system git binary against the source-mirror tree (which IS
  human-readable diff-able .st files).

Removing all 6 tools, all 6 .py templates, and the README section.
Tool count drops 37 -> 31. release_project_version remains the
recommended path for CODESYS-project version control: mirror_export
gives you readable diffs in mcp-mirror/, then standard git commits
+ tags + push, no PDE license required.
2026-04-27 21:43:52 +02:00
Karstein Phobic Nyvold Kvistad
75cf74d317 scaffold(auto-mirror): MirrorCtx + maybeAutoMirror + VSCode integration helpers
In-flight scaffolding for the --auto-mirror feature. Adds:

- ServerConfig.autoMirror flag, wired to --auto-mirror CLI option
- MirrorCtx (autoMirror, scriptManager, executor, workspaceDir,
  openedInVscode set, vscodeCli path)
- findVscodeCli() probes PROGRAMFILES/LOCALAPPDATA/PROGRAMFILES(x86)
  for the code.cmd shim
- maybeOpenMirrorInVscode() spawns 'code --add <mirror>' detached,
  once per mirror dir per session
- maybeAutoMirror() runs mirror_export after a successful edit and
  triggers the VSCode add
- formatModifyingResponse() wrapper around formatToolResponse +
  maybeAutoMirror

No tool wrappers yet -- this is dead code until the modifying tools
are switched from formatToolResponse to formatModifyingResponse.
Doing that as a separate commit so the diff is reviewable.
2026-04-27 21:29:55 +02:00
Karstein Phobic Nyvold Kvistad
f94e63ea8a release: v0.4.8 -- launcher allows multi-install (filter spawn-guard by exe path) 2026-04-27 20:58:00 +02:00
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
Karstein Phobic Nyvold Kvistad
282b261d2f docs(README): drop the no-postinstall-banner footnote 2026-04-27 20:51:34 +02:00
Karstein Phobic Nyvold Kvistad
1145fdc32e release: v0.4.7 -- drop broken postinstall, document the on-demand --print-config flow 2026-04-27 20:46:46 +02:00
Karstein Phobic Nyvold Kvistad
c7f103d3fd revert: drop postinstall banner -- npm 7+ buffers script stdout by design
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.
2026-04-27 20:46:45 +02:00
Karstein Phobic Nyvold Kvistad
c1eabf08a9 release: v0.4.6 -- postinstall actually prints when installing from a repo cwd 2026-04-27 20:40:03 +02:00
Karstein Phobic Nyvold Kvistad
37050ce2b6 fix(postinstall): use script-location-vs-INIT_CWD to detect dev (npm 11)
The previous heuristic checked whether INIT_CWD's package.json had our
name -- but that triggers a false positive when the user runs
`npm install -g codesys-mcp-sp21-plus@latest` from a clone of the
repo (very common -- they're testing the new release). My install
ran from ~/Codesys-MCP and the banner stayed silent.

Right discriminator: does the script's __dirname LIVE INSIDE
INIT_CWD? If yes, this is the dev case (installing yourself into
yourself). If no -- even when INIT_CWD is a clone of this repo --
the script lives in the global prefix and the user is doing a real
install. Print the banner.

Verified:
- INIT_CWD == repo, __dirname == /tmp/...: banner prints
- INIT_CWD == repo, __dirname == repo/dist: silent (dev case)
2026-04-27 20:40:02 +02:00
Karstein Phobic Nyvold Kvistad
5c756147c1 release: v0.4.5 -- postinstall banner actually prints (npm 11+) 2026-04-27 20:37:29 +02:00
Karstein Phobic Nyvold Kvistad
1639741cdd fix(postinstall): banner now actually prints on real installs (npm 11+ compat)
npm 11 stopped setting npm_config_global=true, so the previous guard
`process.env.npm_config_global !== 'true'` always evaluated true and
the banner was silently skipped on every install -- including the
`npm install -g` case it was supposed to handle.

Replace the global-detection (which is unreliable across npm versions)
with a positive dev-clone detector: only skip if INIT_CWD points at a
checkout of this very package (matched by package.json name). Also
keep the CI skip (CI=true / npm_config_ci=true).

Verified all three paths:
- Real install (no INIT_CWD or INIT_CWD outside repo): banner prints
- Dev clone (INIT_CWD = this repo): silent
- CI: silent
2026-04-27 20:37:28 +02:00
Karstein Phobic Nyvold Kvistad
7f7abaf996 release: v0.4.4 -- postinstall now tells you WHERE to paste the snippet 2026-04-27 20:32:31 +02:00
Karstein Phobic Nyvold Kvistad
5d812ea941 feat(postinstall): tell users WHERE to put the snippet (project vs user scope)
After printing the .mcp.json snippet, also print:
- Project-scoped path: <project>/.mcp.json (recommended, git-shareable)
- User-scoped path: %USERPROFILE%/.claude.json (resolved for the current user)
- 'claude mcp add' CLI alternative
- Restart-Claude-Code reminder

The path interpolation uses USERPROFILE so the printed path matches
the user's actual home, not a generic placeholder.
2026-04-27 20:32:30 +02:00
Karstein Phobic Nyvold Kvistad
3d75548ba9 release: v0.4.3 -- postinstall banner with .mcp.json snippet 2026-04-27 20:31:22 +02:00
Karstein Phobic Nyvold Kvistad
b9c1df25b8 feat(postinstall): dump full .mcp.json snippet after global npm install
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.
2026-04-27 20:31:21 +02:00
Karstein Phobic Nyvold Kvistad
a11c58062f release: v0.4.2 -- --print-config CLI for auto-generated .mcp.json snippets 2026-04-27 20:23:59 +02:00
Karstein Phobic Nyvold Kvistad
9c98e61974 feat(cli): --print-config emits ready-to-paste .mcp.json for every detected install
New flags:
- --print-config: scan installs and emit a JSON block per install with
  derived server names (codesys-sp21-patch5, codesys-sp22-patch1, etc.)
- --sp <n>: filter to one SP family; collapses entry name to 'codesys'
  when exactly one install matches
- --name <name>: override the entry name (only valid with --sp narrowing
  to one)

Side effect: --detect now reuses the same detector and additionally
prints the derived profile name + suggested server entry name per
install, so even users sticking to manual config get the values
without guessing.

Refactored install discovery into src/detect.ts so both --detect and
--print-config share one implementation. New unit test fixture covers
version parsing, missing-exe, dedup, sort order, --sp filter behaviour,
--name override constraints, and verifies the emitted JSON parses back
once // comments are stripped.

The output also surfaces the multi-install caveat from launcher.ts:
the double-spawn guard refuses to start a second CODESYS.exe even on
a different exe path, so only one configured entry can be active at
a time.
2026-04-27 20:22:15 +02:00
Karstein Phobic Nyvold Kvistad
d5731512a2 docs(README): Quick Start now shows SP21 example first, then SP22 2026-04-26 22:57:15 +02:00
Karstein Phobic Nyvold Kvistad
2c7eeccb94 fix: drop dead sp21-plus-migration-notes branch refs from generated md + smoke-test doc
Branch was deleted after main caught up. server.ts auto-generates
library.md and pou-dump.md headers in user projects, so the broken
URL was leaking into every consumer of those tools. Now points at
the repo root (main is the only branch).
2026-04-26 22:56:43 +02:00
Karstein Phobic Nyvold Kvistad
ca3062a2d8 docs(README): promote Quick Start (npm install + .mcp.json snippet) above 'What's new' 2026-04-26 22:51:48 +02:00
Karstein Phobic Nyvold Kvistad
fda997e546 release: v0.4.1 -- README restructure (fork notice, what's-new, npm install) 2026-04-26 22:48:21 +02:00
Karstein Phobic Nyvold Kvistad
0352b6c15e chore: sync lockfile to current name + drop stale branch reference from README 2026-04-26 22:48:11 +02:00
Karstein Phobic Nyvold Kvistad
58d45066e4 docs(README): restructure -- fork notice up top, what's-new section, npm install front-and-center
- Lead with a clear "this is a fork" callout (upstream link, fork link, npm link, maintainer)
- New "What's new in this fork" section grouping all changes:
  * Compatibility fixes (SP21+ watcher rewrite, KeyboardInterrupt cancel hardening)
  * Upstream tool fixes (create_folder, compile JSON long, connect_to_device LoginMode + auto-login, ensure_project_open switch, set_pou_code wipe, add_library, list_project_libraries)
  * New tools (mirror_export, bump_project_version, release_project_version, read_running_version_online, git_remote_add, git_branch_set_upstream_to, git_push)
  * Reliability fixes (launcher refuses double-spawn, shutdown kills orphans, git_* persists via project.save)
- Promote npm install to the top of the install section; source install now follows
- MCP Tools tables now mark each tool as **NEW** or **FIXED** so readers can scan what came from this fork
2026-04-26 22:44:06 +02:00
Karstein Phobic Nyvold Kvistad
4432880d42 docs(README): use the literal branch URL in the About blurb 2026-04-26 22:32:16 +02:00
Karstein Phobic Nyvold Kvistad
bb974655ae docs(README): link the active branch + npm registry from the About blurb
The 'sp21-plus-migration-notes' branch name was shown in code-style
but not linked. Made it a hyperlink to
https://github.com/phobicdotno/Codesys-MCP-SP21-plus/tree/sp21-plus-migration-notes
so a reader landing on the README can click straight through to the
live source state without first navigating to the repo root.

Also added a sibling sentence pointing at the published npm registry
page (https://www.npmjs.com/package/codesys-mcp-sp21-plus) so the
About blurb now exposes the three canonical landing pages: upstream,
fork repo (+ branch), and npm.
2026-04-26 22:29:47 +02:00
Karstein Phobic Nyvold Kvistad
a702f34d9d docs(README): make it explicit that 'codesys-mcp-sp21-plus' npm package IS this fork
Reader feedback: the install line 'npm install -g codesys-mcp-sp21-plus'
didn't make clear that the npm package IS this fork (rather than some
unrelated package or the upstream luke-harriman/Codesys-MCP).

Changes to the Installation section:
- Lead paragraph now states explicitly that the fork is published as
  'codesys-mcp-sp21-plus' by phobic, and that this is this fork (not
  upstream, not a Python package).
- 'npm install -g' command moved into a clear 'Install the fork from
  npm' subsection with a bullet list explaining what -g does, where
  the binary lands on PATH, and which deps come along for the ride.
- Added two verification commands (--version, --detect) so a first-time
  user has an obvious next step after install.
- Added an upgrade command ('npm install -g codesys-mcp-sp21-plus@latest')
  so the post-install workflow is fully documented.
- 'From source' subsection retitled 'Install the fork from source' for
  parallel structure.
2026-04-26 22:28:02 +02:00
Karstein Phobic Nyvold Kvistad
700afe27bf docs(README): lead with 'npm install -g codesys-mcp-sp21-plus' now that fork is published
Package is live on the npm registry as of 2026-04-26 (version 0.4.0,
maintainer phobic). The bare-name install line that was previously
warned-against now actually works, so it's the leading install path
again. The git-clone workflow is demoted to a 'From source' subsection
for users who want to track sp21-plus-migration-notes directly,
contribute patches, or run a specific commit.

Verified live:
  npm view codesys-mcp-sp21-plus version  -> 0.4.0
2026-04-26 22:25:22 +02:00
Karstein Phobic Nyvold Kvistad
2e78c45972 docs(README): focus install on git clone; drop npm-publish caveats
User feedback: don't lead with npm registry concepts when the fork
isn't published. Replaced the three-option list (with the misleading
'not currently published' caveat) with a clean two-step git-clone
workflow:

  1. git clone + npm install + npm run build  (mandatory)
  2. wire into .mcp.json via either:
     A) command: 'node', args: ['<path>/dist/bin.js', ...]
        (simplest -- no global state)
     B) command: 'codesys-mcp-sp21-plus' after running 'npm link'
        (only useful if you also want the bin on PATH for ad-hoc use)

Plus a short 'Updating' section that documents what to pull / rebuild
when the upstream branch advances.

Removed entirely:
- 'Option 1: npm install -g github:phobicdotno/...' (still works, but
  drowns the simpler path)
- The 'Heads-up on npm install -g codesys-mcp-sp21-plus' caveat
  paragraph (the new section doesn't mention the registry name at all,
  so the warning has no premise)
2026-04-26 22:18:12 +02:00
Karstein Phobic Nyvold Kvistad
d922238059 docs(README): clear install instructions for the unpublished fork
Three explicit install paths replacing the misleading 'npm install -g codesys-mcp-sp21-plus' top-line (the fork is not published to npm; bare-name install would fail with 404):

1. npm install -g github:phobicdotno/Codesys-MCP-SP21-plus
   -- installs directly from this repo, recommended for end users
2. git clone + npm install + npm run build + npm link
   -- recommended for development; lets src/ edits take effect after a
   build, and Python script edits hot-reload from dist/scripts/ without
   one
3. git clone + npm install + npm run build, then node dist/bin.js
   -- avoids touching global node_modules; full .mcp.json snippet shown

Also clarifies up-front that this is a Node/npm package, not a Python
package -- the .py files under src/scripts/ are CODESYS IronPython
templates bundled into the npm distribution, not a pypi project. There
is no pip install. Requirements section now lists the supported CODESYS
versions explicitly (SP19 / SP21 3.5.21.x / SP22 3.5.22.x) and notes
the PDE subscription gate on the git_* tools.
2026-04-26 22:03:38 +02:00
Karstein Phobic Nyvold Kvistad
405be44a58 rename: Codesys-MCP-SP22+ -> Codesys-MCP-SP21+ + multi-install README + missing tools
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.
2026-04-26 22:02:46 +02:00
Karstein Phobic Nyvold Kvistad
f157e7b61e rename: project -> Codesys-MCP-SP22+
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.
2026-04-26 20:42:15 +02:00
Karstein Phobic Nyvold Kvistad
04b46fb49a fix(read_running_version_online): document the real root cause + fix
Three changes after deeper investigation against PLATEA Win V3:

1. The 'symbol' := 'read' attribute experiment didn't help. Reverted
   bump_project_version's GVL template to plain VAR_GLOBAL +
   qualified_only (matches what shipped originally, minus CONSTANT).
   Comment updated to explain both why CONSTANT is wrong (compile-time
   inlining) and why the symbol attribute alone wasn't enough (it
   requires a Symbol Configuration object to do anything).

2. Real root cause for read_running_version_online's 'Invalid expression':
   CODESYS strips unreferenced GVLs from the online symbol table at
   compile time, regardless of attribute pragmas. The version anchor by
   definition has no IEC code reading it, so the optimizer drops it.
   GVL_Test.bRun reads fine despite no references because GVL_Test has
   OTHER referenced variables; entire-GVL retention seems to be the
   stripping unit, not per-variable.
   Verified end-to-end: adding 'sVersionTag := _MCP_PROJECT_VERSION
   .sVersion;' in PLC_PRG made the read return '1.4.1.0' on PLATEA.

3. Updated the read_running_version_online error message to surface
   BOTH the (now-rare) CONSTANT case AND the (common) unreferenced-GVL
   case, with the exact 2-line code snippet a user needs to paste into
   their main program. The bump tool intentionally does NOT auto-inject
   this -- modifying user code on every release was deemed too invasive.
   Documented the requirement in TEST_OVERVIEW.md alongside the v5
   sweep notes.

37/37 tests still green. v1.4.2.0 of MCPTest2 carries the working
PLC_PRG reference as the canonical demonstration.
2026-04-26 20:13:25 +02:00
Karstein Phobic Nyvold Kvistad
ef259c8ee3 fix(online tools): auto-login + non-CONSTANT version GVL
Two related v5-sweep fixes for the online/runtime tool family:

1. Auto-login helper for headless mode

   In headless mode each MCP call spawns a fresh CODESYS --noUI process,
   so the login state established by connect_to_device dies before the
   next call. Pre-fix, only connect_to_device and download_to_device did
   their own login(); the other four (start_stop_application,
   read_variable, write_variable, read_running_version_online) silently
   failed in headless with 'Application not logged in.' (start/stop) or
   'Invalid expression' (read/write). They worked in persistent mode
   only because the login carried across calls.

   Added ensure_logged_in(online_app, login_wait_seconds=30) to
   ensure_online_connection.py. Idempotent: short-circuits via
   online_app.is_logged_in (persistent mode is a no-op, no extra login
   roundtrip). When not logged in, runs the same enum-probe + call-shape
   probe + STABLE_STATES settle-wait pattern as connect_to_device.py.
   Added to start_stop_application.py, read_variable.py,
   write_variable.py, read_running_version_online.py.

2. _MCP_PROJECT_VERSION GVL emitted as plain VAR_GLOBAL, not CONSTANT

   CODESYS inlines VAR_GLOBAL CONSTANT scalars at compile time and
   strips them from the online symbol table. The whole point of
   _MCP_PROJECT_VERSION.sVersion is to be readable live from the
   running PLC, so CONSTANT was the wrong storage class.
   read_running_version_online failed against EVERY project bumped via
   the old template -- 'Invalid expression' on the runtime read.

   Dropped CONSTANT from VERSION_GVL_DECLARATION_TEMPLATE in
   bump_project_version.py. Existing projects auto-migrate on the next
   bump because maintain_version_gvl()'s existing-GVL branch overwrites
   textual_declaration with the (now non-CONSTANT) template. The string
   is still effectively read-only at runtime -- only the bump tool
   updates it.

   read_running_version_online.py also got a more precise error message
   that explicitly fingerprints the 'Invalid expression' failure mode
   and points at the CONSTANT root cause. Useful for any user landing
   on a project that pre-dates this fix.

Verified end-to-end against local CODESYS Control Win V3 (PLATEA, port
11740) on MCPTest2 v1.3.4.0:
- connect_to_device, get_application_state, download_to_device,
  start_stop_application (both directions), read_variable
  (PLC_PRG.watchdog1 = 225 ticking), write_variable (200 -> 204 in 4s
  proves write took), disconnect_from_device: all 7 PASS.
- read_running_version_online failure reproduced (CONSTANT inlined),
  fix landed -- next bump on MCPTest2 will validate.

37/37 unit/integration tests green. TEST_OVERVIEW.md updated with the
v5 device sweep, with the headless-mode deep-dive, and with the
broken-by-design notes on read_running_version_online.
2026-04-26 19:54:13 +02:00
Karstein Phobic Nyvold Kvistad
d414c779a5 fix(add_library): refuse to save unresolvable placeholders
Prior behaviour: lm.add_library(LIBRARY_NAME) was called with a string,
which always hits the placeholder overload of ScriptLibManObject.add_library
(see helpme-codesys.com "ScriptLibManObject" / local SP22 stub
ScriptLib/Stubs/scriptengine/ScriptLibManObject.pyi). If the named
placeholder is not registered in the IDE, the resulting reference fails
to resolve at load time and the next project open throws

  Library Manager: Error: Could not open library 'X'.
  (Reason: The placeholder library 'X' could not be resolved.)

after which script_engine.projects.primary returns None and the project
is effectively bricked until the binary is reverted. add_library reported
SUCCESS in this scenario.

Fix:
  1. Pre-resolve LIBRARY_NAME via the IDE-level
     library_manager.find_library(name) global. If found, pass the
     resulting ManagedLib to lm.add_library(...) -- the V3.5.5.0 ManagedLib
     overload which produces a managed reference instead of a placeholder.
  2. After the add, walk lm.references to locate the new entry and verify
     it resolved (managed -> always; placeholder -> non-empty
     effective_resolution per ScriptPlaceholderReference in the stub).
  3. If the reference did not resolve, call lm.remove_library(name) to back
     out the bad reference and refuse to save the project, returning an
     actionable error instead.

Also tightened the libman lookup to use the documented
has_library_manager / get_library_manager() container API
(ScriptLibManObjectContainer in the stub) instead of name-searching for
"Library Manager" as a tree node, matching what list_project_libraries.py
already does. The legacy name-search fallback is preserved for older SPs.

Regression test added in tests/integration/e2e.test.ts asserting the
rendered script template carries the resolution gate, the managed-overload
preference, the back-out call, and that primary_project.save() in the
add_library body lives downstream of the _is_resolved gate.

Followup: lm.remove_library(name) is documented for SP22 and is the
clean back-out path. On SPs that lack remove_library (none observed in
the 3.5.21+ docs but possible on truly old branches) the script reports
the constraint and exits non-zero rather than silently saving a bad
reference.
2026-04-26 19:44:00 +02:00
Karstein Phobic Nyvold Kvistad
f7d564bc47 test(bench): v5 sweep -- fix set_pou_code params, all 10 cases pass
Three changes:
1. Fix the bench harness's set_pou_code case. The previous version
   passed POU_PATH / DECLARATION_CODE / IMPLEMENTATION_CODE, but the
   script template expects POU_FULL_PATH / DECLARATION_CONTENT /
   IMPLEMENTATION_CONTENT. Plus the recently-added SET_DECLARATION /
   SET_IMPLEMENTATION boolean flags from 35abc8c. With these
   corrections, set_pou_code passes for the first time in the bench.
2. Add bench-results-v5.json from a fresh persistent-mode run on
   MCPTest2 v1.3.4.0. All 10 cases PASS.
3. Update TEST_OVERVIEW.md with the v5 numbers alongside the v1
   historical numbers. v5 is ~2x faster across the board than v1
   (likely the cumulative effect of the ScriptManager cache removal +
   SP22 Patch 1 IPC improvements). first-cold open_project is slower
   than v1 (10.6 s vs 7.7 s) -- one-shot, noise probably; warm
   already-open call dropped from 740 ms to 314 ms.

The v5 set_pou_code pass is the third end-to-end signal that the
omitted-decl wipe fix is good (after the e2e regression test and the
live PLC_PRG restoration earlier today).
2026-04-26 19:30:48 +02:00
Karstein Phobic Nyvold Kvistad
888a035c0c feat(list_project_libraries): capture project compiler version
Calls primary_project.get_compilerversion() (ScriptEngine 4.2.0.0+) and
emits the result through the JSON payload. Renders as:

  - library.md: a 'Project compiler version' row in the Versions table
  - list_project_libraries chat output: a 'Compiler version: X.Y.Z.W'
    line in the header section

Motivation: changing the project's compiler version (Project > Project
Settings > Compiler version, or set_compilerversion_to_newest()) only
touched the .project binary -- mirror_export couldn't see it, so the
release classifier had to fall back to SHA comparison and emitted the
generic 'device-tree / library refs / task config / visu / Save() touch'
classification. Compiler-version changes now leave a textual diff in
mcp-mirror/library.md, letting the classifier issue an honest revision
bump instead of the bare build-bump SHA fallback.

Defensive: get_compilerversion() is wrapped in try/except so older
ScriptEngines (< 4.2.0.0) that lack the method don't crash the tool;
they just emit compiler_version=null and the field is omitted from
output.
2026-04-26 19:18:31 +02:00
Karstein Phobic Nyvold Kvistad
35abc8cb52 fix(set_pou_code): omitted declaration/impl no longer wipes the POU
Bug: calling set_pou_code with implementationCode only (declarationCode
omitted) would wipe the POU's PROGRAM/VAR...END_VAR block in the binary.
After such a call mirror_export classified the POU as 'UNKNOWN' (no
PROGRAM/FUNCTION_BLOCK keyword in the empty declaration), and the var
block disappeared from the .st mirror file.

Root cause: the TS wrapper substituted '' (empty string) into the Python
template when declarationCode was undefined, giving DECLARATION_CONTENT
= "". The Python script then took the truthy-ish branch (empty string
is not None) and called decl_obj.replace('') -- wiping textual_decl.

Fix: pass explicit SET_DECLARATION / SET_IMPLEMENTATION boolean flags
from the TS wrapper, gate the replace() calls on those flags. Empty
string remains a valid intentional value (caller wants to wipe).

- Reproduced on MCPTest2: PLC_PRG declaration block was wiped between
  v1.3.0.0 and v1.3.1.0 by exactly this code path.
- Regression test added in tests/integration/e2e.test.ts covering the
  omitted-declarationCode path.
- Existing set_pou_code test extended to assert SET_DECLARATION /
  SET_IMPLEMENTATION are emitted in the rendered script.
2026-04-26 18:41:48 +02:00
Karstein Kvistad
26260ac43d fix(release_project_version): preserve real newlines in tag body via -F tempfile
The dual-SHA tracking commit (146d950) wrote the tag annotation with
`git tag -a -m JSON.stringify(body)`. JSON.stringify escapes newlines
as the literal two-char sequence "\n", and the shell passes those
through unchanged -- so git stored the body as one big line with
literal "\n" chars instead of real LF bytes.

The reader (readTagShas) used a multiline regex anchored on `^` and
`$`, which doesn't match across literal "\n" -- so v1.3.2.0's tag was
written with SHAs in the body but they're invisible to the next
release's read-back.

Two fixes:
  - Reader (readTagShas): normalise literal "\n" sequences to real
    newlines before applying the regex. Backward-compatible -- handles
    the v1.3.2.0 tag transparently and works on properly-formed tags
    from v1.3.3.0 onward too.
  - Writer (release_project_version step 8): write the body to a
    temp file and use `git tag -F <tempfile> --cleanup=verbatim` so
    real LF bytes go in. Also adds os import for os.tmpdir().

Verified locally: dist/server.js loads cleanly. End-to-end behaviour
confirmable on the next release_project_version call against any
project.
2026-04-26 18:14:33 +02:00
Karstein Kvistad
146d950e17 feat(release_project_version): dual SHA-256 tracking for binary + mirror
Implements bidirectional change detection for the release pipeline.

Two SHA-256 fingerprints are now stored in every release tag's annotated
body:
  project-sha256: <hash of the .project binary>
  mirror-sha256:  <hash of the mcp-mirror/ tree>

On the next release_project_version call, these are read back via
git cat-file -p <prior-tag> and compared against the current values
to detect three classes of change that the mirror-only diff missed:

  (a) binary changed AND mirror unchanged (working tree, before
      mirror_export). Normal "user edited in IDE" path. Classifier
      handles this as it always did.

  (b) binary unchanged AND mirror changed (working tree, before
      mirror_export). User edited .st files in mcp-mirror/ directly
      with a text editor. mirror_export is about to overwrite those
      edits, so we surface a WARNING in the release log. Future:
      a mirror_import tool would push these back into the binary;
      until then, mirror is one-way (binary -> mirror).

  (c) binary changed AND mirror UNCHANGED after mirror_export. The
      .project binary has a non-textual change that mirror_export
      doesn't capture: device tree, library refs, task config,
      visualizations, OPC UA / symbol config, application composer,
      or just a Save() touch (CODESYS embeds timestamps). Classifier
      sees no diff but project SHA flipped. Promote 'no-changes' to
      a build-level bump so the version still ticks. The Changelog
      entry calls out the SHA-fallback evidence so it's visible in
      review.

Helper functions added at module scope:
  - sha256OfFile(filePath): single-file SHA-256.
  - sha256OfDirectory(dirPath): deterministic tree walk, sorts
    entries by name, hashes (relative-path, content) pairs separated
    by NULs.
  - readTagShas(projectDir, tagName): parses project-sha256 /
    mirror-sha256 lines out of an annotated tag body. Returns
    undefined for either field if missing -- gracefully handles
    older tags that don't carry the fingerprints.

The dual-SHA approach was suggested by the user after observing that
a manual edit to MCPTest2.project (made via the IDE) wasn't surfaced
by the mirror-only classifier when the change happened to be in a
non-mirrored region (likely device tree or library refs).

Verification pending: needs a vsc reboot to load the new server.js
into the running MCP process. Once reloaded, the next call against
v1.3.1.0 should:
  - Read priorShas from the v1.3.1.0 tag (likely empty since this
    is the first release with the new tag format).
  - Treat empty priorShas as "no info, can't fall back" and behave
    exactly like the pre-fix orchestrator. So nothing breaks.
  - Write project-sha256 + mirror-sha256 into the v1.3.2.0+ tag bodies.
  - From v1.3.2.0 onward, all three change-detection cases work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:04:55 +02:00
Karstein Kvistad
9c6d28c146 docs(TEST_OVERVIEW): mark create_folder fixed end-to-end + iteration story
- Bumped status from "fixed (factory fallback)" to "fixed in c87f3a9
  (4-iteration debug saga)" with the v1->v4 iteration table:
  * v1 2607063: name= kwarg -- FAIL (SP22 wants foldername)
  * v2 e07f281: positional + foldername= -- FAIL (silent None return)
  * v3 32e6120: project-level + cache removal -- FAIL (still None=fail)
  * v4 c87f3a9: walk children to detect side-effect success -- PASS
- Real root cause documented: SP22's create_folder returns void; the
  fix verifies by walking parent.get_children(False).
- Side benefit from v3 (ScriptManager cache dropped) noted as the
  hot-reload mechanism that made v4 reachable in one debug session.
- "Lesson for future fork work" callout: probably applies to other
  create_* methods too, audit pending.
- Inventory table row for create_folder updated with the c87f3a9
  link + a more accurate description of the strategy chain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:25:22 +02:00
Karstein Kvistad
c87f3a9179 fix(create_folder): WORKING -- create_folder returns void, walk children to detect success
The v3 fix from 32e6120 still fell through every strategy because the
None-as-failure check was wrong. CODESYS scripting's create_folder
methods (both ScriptObject.create_folder and
ScriptProject.create_folder) RETURN VOID -- the folder is created via
side effect; the return value is Python None. v1/v2/v3 all treated
None as "this strategy didn't work" and tried the next, eventually
giving up.

v4 fix: after each create call, walk parent_object.get_children(False)
looking for a child whose get_name() == FOLDER_NAME. First strategy
that produces such a child wins; the rest are skipped (avoids
duplicates). The strategy order also got reshuffled: parent.create_folder
positional is now strategy 1 (per the SP22 ScriptObject stub
signature), with project-level fallbacks behind it.

Verified end-to-end on MCPTest2 + SP22 P1:
  > create_folder(folderName='Test_Bench_Folder',
                  parentPath='PLCWinNT/Plc Logic/Application')
  Folder 'Test_Bench_Folder' created [...]. Project saved.
  > delete_object(objectPath='.../Test_Bench_Folder')
  Object [...] deleted [...]. Project saved.

Per-version trace of the iteration:
  v1 (2607063): name= kwarg -> "create_folder() got an unexpected
                                kwarg 'name'" -- SP22 stub uses
                                foldername.
  v2 (e07f281): positional / foldername= -> silent None return on
                                Application; fell through.
  v3 (32e6120): added primary_project.create_folder(name, SV_POU)
                first; also dropped ScriptManager cache for hot-reload.
                Still treated None as failure.
  v4 (this):    walk children after each call; succeeds.

Memory note for the kit: the SP22 scripting API has a class of
methods that mutate via side effect and return void. When porting
fork scripts, ALWAYS verify by walking children, never by checking
the return value of create_*. Same pattern probably applies to
create_pou / create_dut / create_gvl too -- worth a separate audit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:23:26 +02:00
Karstein Kvistad
32e612000d fix(create_folder): try project.create_folder(name, SV_POU) first; drop ScriptManager cache
create_folder v2 (positional foldername) returned None silently against
the SP22 Application object -- no exception raised, no folder created.
Investigation showed:
  - ScriptObject.create_folder(foldername) is documented to "create a
    folder in the structured view of the parent node", but on Application
    specifically it's a silent no-op (the structured view isn't pinned to
    POU view there).
  - ScriptProject.create_folder(foldername, structured_view=None) on the
    project itself with explicit SV_POU GUID
    ({21AF5390-2942-461a-BF89-951AAF6999F1}) is the documented and
    reliable pathway -- the resulting folder appears under Application
    in the IDE tree because that's where SV_POU lives.

v3 fix: try strategies in order until one returns non-None:
  (1) primary_project.create_folder(name, SV_POU_GUID) -- new, primary
  (2) parent.create_folder(name) positional -- pre-SP21 path
  (3) parent.create_folder(foldername=name) -- alt keyword
  (4) primary_project.create_folder(name) -- default view
  (5) parent.create_object(typeUuid='85d1215e-...') -- alt factory
  (6) parent.add(script_engine.types.IecFolder, name=name) -- legacy
Each strategy guards on hasattr + return-value-not-None, so a silent
no-op falls through instead of being mistaken for success.

ScriptManager: dropped the in-memory template cache. Each loadTemplate
call now reads the .py from disk fresh. Cost: ~1ms per call vs ~1.5s
of CODESYS execution time -- invisible. Win: edits to dist/scripts/
take effect without an MCP restart, which makes iterating on script-
side fixes (like this very create_folder loop) much faster. Existing
"cache hit" unit test rewritten as "two loads return equal content".

tests/test-fixes.mjs: standalone harness that drives a single persistent
CODESYS through HeadlessExecutor + CodesysLauncher to verify the four
broken-tool fixes end-to-end. Useful for regression-testing without
needing a vsc reboot loop. Currently only smoke-tests
create_folder + compile + cross-project; expand as more fixes need
verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:16:44 +02:00
Karstein Kvistad
e07f281fd0 fix(create_folder): use positional / foldername= instead of name= kwarg
The v1 fix from 2607063 used `parent.create_folder(name=FOLDER_NAME)`
which raised "create_folder() got an unexpected keyword argument 'name'"
on SP22 -- the actual stub signature in
  C:\Program Files\CODESYS 3.5.22.10\CODESYS\ScriptLib\Stubs\scriptengine\ScriptObject.pyi
is
  def create_folder(self, foldername): ...

The fallback chain (create_object + add(IecFolder)) caught the failure
and reported correctly, so no damage -- but the primary path was wrong.

Verified via the SP22 stubs that the keyword is `foldername`. Use
positional form first (agnostic to the keyword name across SP releases),
fall through to foldername= if positional fails for some reason, then
keep the existing alternate-factory chain as deeper fallbacks.

Tested partially in this session: create_folder DEBUG output confirmed
the v1 fix's fallback chain was running as designed; the keyword fix
will be runtime-verified on the next vsc reboot since script-manager
caches dist/scripts at MCP startup.

Verification trace (v1 against SP22):
  DEBUG: Calling parent.create_folder(name='Test_Bench_Folder')
  WARN: parent.create_folder() raised: create_folder() got an
        unexpected keyword argument 'name'

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:06:11 +02:00
Karstein Kvistad
0fc23ef8cd docs(TEST_OVERVIEW): real bench numbers + mark broken tools as fixed
- Headline result table at the top: persistent is 15-24x faster than
  headless across all CODESYS-roundtripping tools. Numbers are from a
  fresh bench-results.json run on MCPTest2.
- Updated mode primer with measured first-call vs warm-call costs:
  persistent first launch ~14.6s, subsequent calls ~1.5s; headless
  first cold call ~58s, warm calls ~22s.
- Inventory table: replaced "typical" estimates with measured numbers
  for the 9 tools the bench covers.
- Deep-dive section: each broken tool now marked "fixed in 2607063"
  with concise summary of the fix that landed (instead of "proposed
  fix"). Removed the long pre-fix code blocks since they're in the
  commit history now.
- Status legend: create_folder, compile_project, get_compile_messages,
  connect_to_device, open_project all flipped from broken to fixed
  (with caveats: some need PLC or runtime verification).
- Footnote about set_pou_code bench-harness failure (multi-line code
  through triple-quoted-string interpolation -- not a tool bug).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:58:05 +02:00
Karstein Kvistad
2607063306 fix: 4 broken-tool fixes (compile json long, connect_to_device LoginMode,
create_folder SP21+ fallback, ensure_project_open cross-project switch)
+ bench results

compile_project.py + get_compile_messages.py:
- IronPython 2.7's json.dumps cannot serialize System.Int64-backed `long`
  values, which is what CODESYS's compile-message objects expose as
  line_number / position. Added _coerce_int + _coerce_str helpers and a
  shared _build_message_entry function. Three duplicated message-building
  blocks collapsed into single helper calls.
- Defensive `try: json.dumps(...) except TypeError: json.dumps(default=str)`
  so a stray field that slips past the helpers doesn't kill the emit.

connect_to_device.py:
- SP21+ may expose the login enum as LoginMode rather than
  OnlineChangeOption. Extended the candidate sweep to probe both
  script_engine.LoginMode and script_engine.OnlineChangeOption AND
  online_app.LoginMode/OnlineChangeOption (some builds attach it to the
  app object). Added "OnlineChange" + "Login" + "Download" to the
  preferred-priority list. Added a 3-arg call shape variant for SPs
  that take (mode, secondary-mode, force-bool).

create_folder.py:
- parent_object.create_folder() is no longer exposed on every parent type
  in SP21+. Added two fallback factories tried in order:
    1. parent.create_object(typeUuid='85d1215e-6520-4983-9a55-2d39d1f24cb4', name=...)
    2. parent.add(script_engine.types.IecFolder, name=...)
  with detailed warnings when each path fails. Final TypeError now lists
  every factory tried so a future SP rotation surfaces clearly.

ensure_project_open.py:
- Uncommented the close-prior-project branch (was a TODO since the
  initial fork). Cross-project switches in a persistent CODESYS now do
  save() -> close() -> 500ms pump -> open(target). Without this,
  projects.open against a different already-primary project fails
  intermittently on file lock contention or pops a "project in use"
  modal that freezes the IDE thread.
- save() is best-effort: if it raises (transient lock, save-as required)
  we still proceed with close + open rather than getting stuck in a
  half-switched state forever.

tests/bench-results.json:
- Captured timings from a clean run on MCPTest2 (PLCWinNT, 5 lib refs,
  ~12 POUs). 9 tools x 2 modes x iterations.
  Headers (mean ms): persistent vs headless --
    open_project              7700  vs   40041   (~5x; first call cold)
    mirror_export             1547  vs   23723   (~15x)
    list_project_libraries    1565  vs   23322   (~15x)
    get_all_pou_code          1607  vs   23376   (~15x)
    save_project              2095  vs   23321   (~11x)
    create_pou (FB)           1540  vs   23903   (~16x)
    delete_object             1544  vs   27420   (~18x)
    bump_project_version      1540  vs   30678   (~20x)
    bump_project_version #2   1556  vs   37769   (~24x)
- set_pou_code FAILED in both modes -- bench harness param-shape issue
  (multi-line code passed verbatim to triple-quoted-string interpolation
  doesn't survive the round-trip). Tool itself works fine through the
  MCP tool call path; bench needs to escape newlines / use the same
  prepareScriptWithHelpers shape the server uses. Filed for follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:53:25 +02:00
Karstein Kvistad
2e2ab00ae1 docs+test: TEST_OVERVIEW + bench harness for headless vs persistent
TEST_OVERVIEW.md: complete tool inventory (37 tools), each tagged
working / broken with status notes and per-mode timing characteristics.
Mode primer up top explains the per-call vs first-call cost asymmetry.

Deep-dive on every broken tool with proposed fixes:
  - create_folder: parent_object.create_folder() not exposed in SP21+;
    fall back to create_object(typeUuid=...) or types.IecFolder.
  - compile_project / get_compile_messages: IronPython 2.7 json.dumps
    can't serialize System.Int64 (line_number / position fields).
    Fix is a _coerce_int helper applied uniformly.
  - connect_to_device: SP21+ may expose the login enum as LoginMode
    instead of OnlineChangeOption. Extend the candidate sweep over
    multiple enum sources, with priority on TryOnlineChange-equivalents.
  - open_project (cross-project switch): ensure_project_open has the
    "close prior project" branch commented out; uncomment with a
    save+close+delay sequence and silent-mode guard.

list_project_libraries is flagged as  working in current SP22
(historical entries in the project memory should be cleared).

bench.mjs: standalone benchmark harness driving HeadlessExecutor and
CodesysLauncher directly (no MCP server in the loop). Copies the source
.project to a temp dir so write tools don't mutate the original. Covers
9 tools (read-only + write-revertible) with configurable iterations,
emits markdown to stdout + JSON to --out.

Run with:
  node tests/bench.mjs --modes headless,persistent --iterations 2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:41:52 +02:00