0
0
Fork 0

feat(read_running_version_online): read _MCP_PROJECT_VERSION from running PLC

New MCP tool that reads the running project's version from a
connected PLC over the CODESYS online protocol (port 11740 / gateway).
Returns the value of `_MCP_PROJECT_VERSION.sVersion` -- the runtime
anchor that bump_project_version maintains automatically (commit
00d2dd8). Closes the loop on the version-tracking convention:

  bump_project_version           writes _MCP_PROJECT_VERSION.sVersion
                                 into the project (compiled into the
                                 boot application on next download)
  read_running_version_online    reads the same symbol back from the
                                 running PLC over the online protocol

Pairs with the existing connect_to_device + read_variable pattern
but with sharpened error messages tailored to the version-read use
case:
  - missing GVL                  -> 'has bump_project_version run on
                                     this project? Or has the boot
                                     application not been downloaded
                                     since the bump?'
  - read returns None            -> 'variable exists in project but
                                     not in boot app -- download_to_device
                                     after the last bump.'
  - missing online API           -> typed clearly, suggests SP-version
                                     drift.

Implementation: ensure_project_open + ensure_online_connection +
online_app.read_value('_MCP_PROJECT_VERSION.sVersion'); strips quote
characters from the returned STRING; sanity-checks the shape against
\b\d+\.\d+\.\d+\.\d+\b and warns if it doesn't match the
4-part convention. TS handler extracts the matched RUNNING_VERSION
line from the script output and surfaces it as the headline of the
tool response.

SSH transport variant (read_running_version_ssh) lands as a separate
later commit once a real PFC is reachable to test against.
This commit is contained in:
Karstein Phobic Nyvold Kvistad 2026-04-26 00:33:22 +02:00
parent 00d2dd8d96
commit e29bb7e534
2 changed files with 104 additions and 0 deletions

View file

@ -0,0 +1,80 @@
import sys, scriptengine as script_engine, os, traceback, re
# Reads the running project's version from the PLC over the CODESYS online
# protocol (port 11740 on a soft PLC; gateway-resolved on real hardware).
# Looks at the standard runtime anchor maintained by bump_project_version:
#
# _MCP_PROJECT_VERSION.sVersion : STRING
#
# Returns the string value plus a sanity check (matches X.Y.Z.W shape).
# Soft-fails if:
# - the project hasn't been bumped yet (GVL missing) -> clear hint
# - the application isn't downloaded -> clear hint
# - the connection drops mid-read -> error surfaced
VARIABLE_PATH = "_MCP_PROJECT_VERSION.sVersion"
VERSION_PATTERN = re.compile(r"\b(\d+\.\d+\.\d+\.\d+)\b")
try:
print("DEBUG: read_running_version_online: Project='%s'" % PROJECT_FILE_PATH)
primary_project = ensure_project_open(PROJECT_FILE_PATH)
online_app, target_app = ensure_online_connection(primary_project)
app_name = getattr(target_app, 'get_name', lambda: 'Unknown')()
print("DEBUG: connected to application '%s'" % app_name)
# Read the version anchor
raw_value = None
if hasattr(online_app, 'read_value'):
try:
result = online_app.read_value(VARIABLE_PATH)
if result is not None:
if hasattr(result, 'value'):
raw_value = result.value
else:
raw_value = result
except Exception as e:
msg = str(e)
if 'not found' in msg.lower() or 'unknown' in msg.lower() or 'symbol' in msg.lower():
raise RuntimeError(
"Variable '%s' not found on the running PLC. "
"Either bump_project_version has never been run on this project "
"(run it first to create the GVL), or the running boot application "
"predates the GVL (download_to_device after the bump to publish "
"the new symbol). Underlying error: %s" % (VARIABLE_PATH, e)
)
raise
elif hasattr(online_app, 'read'):
try:
raw_value = online_app.read(VARIABLE_PATH)
except Exception as e:
raise RuntimeError(
"Failed to read '%s' via .read(): %s" % (VARIABLE_PATH, e))
else:
raise TypeError(
"Online application object does not expose read_value() or read(). "
"This SP/install may have a different online API surface.")
if raw_value is None:
raise RuntimeError(
"read_value returned None for '%s'. Often means the variable exists in "
"the project but the running boot application doesn't include it -- "
"did you download_to_device after the last bump?" % VARIABLE_PATH)
version_str = str(raw_value).strip().strip("'\"")
matches_shape = bool(VERSION_PATTERN.match(version_str))
print("RUNNING_VERSION: %s" % version_str)
print("Variable: %s" % VARIABLE_PATH)
print("Application: %s" % app_name)
print("Shape check (X.Y.Z.W): %s" % ("OK" if matches_shape else "WARN -- value does not look like a 4-part version"))
print("SCRIPT_SUCCESS: read_running_version_online complete.")
sys.exit(0)
except Exception as e:
detailed = traceback.format_exc()
msg = "Error reading running version from project '%s': %s\n%s" % (
PROJECT_FILE_PATH, e, detailed)
print(msg)
print("SCRIPT_ERROR: %s" % msg)
sys.exit(1)

View file

@ -1553,6 +1553,30 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
}
);
s.tool(
'read_running_version_online',
"Reads the running project's version from a connected PLC over the CODESYS online protocol (port 11740 / gateway). Returns the value of `_MCP_PROJECT_VERSION.sVersion` -- the runtime anchor that bump_project_version maintains automatically. Use this to confirm what version the live PLC is actually running, independently of whatever's in the .project file or the mcp-mirror/. Provides actionable error messages when the GVL is missing (project never bumped) or the boot application is stale (downloaded before the last bump). Implementation: ensure_project_open + ensure_online_connection + read_value('_MCP_PROJECT_VERSION.sVersion').",
{
projectFilePath: z.string().describe("Path to the project file. The tool opens it if not already primary, connects to its configured device, and reads the version anchor."),
},
async (args: { projectFilePath: string }) => {
const escaped = resolvePath(args.projectFilePath, workspaceDir);
const script = scriptManager.prepareScriptWithHelpers(
'read_running_version_online',
{ PROJECT_FILE_PATH: escaped },
['ensure_project_open', 'ensure_online_connection']
);
const result = await executor.executeScript(script);
// Pull the version out of the script's RUNNING_VERSION line
const match = /RUNNING_VERSION: (\S+)/.exec(result.output || '');
const version = match ? match[1] : '?';
return formatToolResponse(
result,
`Running version on PLC: ${version}\n(read from _MCP_PROJECT_VERSION.sVersion via CODESYS online protocol)`
);
}
);
// ─── Filesystem mirror (Phase 1: read-only export) ────────────────────
s.tool(