diff --git a/src/scripts/read_running_version_online.py b/src/scripts/read_running_version_online.py new file mode 100644 index 0000000..c05e47b --- /dev/null +++ b/src/scripts/read_running_version_online.py @@ -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) diff --git a/src/server.ts b/src/server.ts index dd65591..ef74711 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1553,6 +1553,30 @@ export async function startMcpServer(config: ServerConfig): Promise { } ); + 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(