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:
parent
00d2dd8d96
commit
e29bb7e534
2 changed files with 104 additions and 0 deletions
80
src/scripts/read_running_version_online.py
Normal file
80
src/scripts/read_running_version_online.py
Normal 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)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue