0
0
Fork 0

feat: add update_all_libraries MCP tool to bulk-rewrite library versions

Walks the project's Library Manager and rewrites every library reference's
version to a single target value (default '*' = always-newest installed).
The immediate motivating case: X33 has a StringUtils 3.5.18.0 pin that
no longer resolves on SP21/SP22 boxes, plus several other version-pinned
references that should track the locally-installed copies.

  - src/scripts/update_all_libraries.py:
      Defensive multi-pattern enumeration -- ScriptLibraryManager exposes
      different ref-listing methods across SP versions (get_all_libraries
      / get_libraries / get_references / iteration). Same for the per-ref
      version-mutation API: set_version / update_to_version / update /
      set_resolution. Falls back to remove + re-add if no in-place setter
      is exposed.
      Skips refs flagged is_system unless INCLUDE_SYSTEM=True (system
      pins are usually device-tied and changing them breaks the project).
      Emits a per-library before/after table; a single failure makes the
      whole tool report SCRIPT_ERROR so partial-update states are loud.

  - src/server.ts:
      Wires as 'update_all_libraries' with optional targetVersion (default
      '*') and includeSystem (default false). Uses ensure_project_open so
      the tool works against any project on disk, not just the currently
      loaded one.

Companion 'set_library_version' (single-library surgical change) coming
in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Karstein Phobic Nyvold Kvistad 2026-04-25 16:25:33 +02:00
parent 79c83fd4c3
commit 9a4e96f11a
2 changed files with 209 additions and 0 deletions

View file

@ -0,0 +1,180 @@
import sys, scriptengine as script_engine, os, traceback
TARGET_VERSION = "{TARGET_VERSION}"
INCLUDE_SYSTEM = {INCLUDE_SYSTEM}
try:
print("DEBUG: update_all_libraries: TargetVersion='%s', IncludeSystem=%s, Project='%s'" % (
TARGET_VERSION, INCLUDE_SYSTEM, PROJECT_FILE_PATH))
primary_project = ensure_project_open(PROJECT_FILE_PATH)
if not TARGET_VERSION:
raise ValueError("Target version is empty.")
# Locate Library Manager (same pattern as add_library.py).
lib_manager = None
try:
found = primary_project.find("Library Manager", True)
if found:
lib_manager = found[0]
except Exception as e:
print("DEBUG: find('Library Manager') failed: %s" % e)
if lib_manager is None:
try:
for child in primary_project.get_children(True):
nm = getattr(child, 'get_name', lambda: '')()
if 'library' in nm.lower() and 'manager' in nm.lower():
lib_manager = child
break
except Exception as e:
print("DEBUG: child search for Library Manager failed: %s" % e)
if lib_manager is None:
raise RuntimeError("Library Manager not found in project.")
# Enumerate library references. The exact method name varies across SP
# versions, so probe several.
refs = []
for method_name in ('get_all_libraries', 'get_libraries', 'get_references', 'libraries', 'references'):
attr = getattr(lib_manager, method_name, None)
if attr is None:
continue
try:
value = attr() if callable(attr) else attr
refs = list(value)
print("DEBUG: enumerated via %s -> %d refs" % (method_name, len(refs)))
break
except Exception as e:
print("DEBUG: %s() failed: %s" % (method_name, e))
if not refs:
try:
refs = list(lib_manager)
print("DEBUG: enumerated via iter(lib_manager) -> %d refs" % len(refs))
except Exception:
pass
if not refs:
raise RuntimeError("Could not enumerate library references via any known API.")
def _attr(obj, name, default=''):
v = getattr(obj, name, None)
if v is None:
return default
try:
return v() if callable(v) else v
except Exception:
return default
def _is_system(ref):
v = getattr(ref, 'is_system', None)
if v is None:
return False
try:
return bool(v() if callable(v) else v)
except Exception:
return False
results = []
ok = 0
skipped = 0
failed = 0
for ref in refs:
try:
name = _attr(ref, 'get_name', '?')
if name == '?':
name = _attr(ref, 'name', '?')
old_version = str(_attr(ref, 'get_version', '?'))
if old_version == '?':
old_version = str(_attr(ref, 'version', '?'))
namespace = _attr(ref, 'get_namespace', '')
if not namespace:
namespace = _attr(ref, 'namespace', '')
if _is_system(ref) and not INCLUDE_SYSTEM:
results.append((name, namespace, old_version, old_version, "skipped (system)"))
skipped += 1
continue
# Try in-place update methods first.
updated = False
last_err = None
for method_name in ('set_version', 'update_to_version', 'update', 'set_resolution'):
if not hasattr(ref, method_name):
continue
try:
getattr(ref, method_name)(TARGET_VERSION)
updated = True
print("DEBUG: %s.%s('%s') OK" % (name, method_name, TARGET_VERSION))
break
except Exception as e:
last_err = e
print("DEBUG: %s.%s('%s') failed: %s" % (name, method_name, TARGET_VERSION, e))
# Fallback: remove + re-add at the new version.
if not updated:
try:
if hasattr(lib_manager, 'remove_library'):
lib_manager.remove_library(ref)
elif hasattr(lib_manager, 'remove'):
lib_manager.remove(ref)
else:
raise RuntimeError("no remove_library / remove method")
added = False
for add_name in ('add_placeholder_library', 'add_library', 'insert_library'):
if not hasattr(lib_manager, add_name):
continue
try:
getattr(lib_manager, add_name)(name, TARGET_VERSION)
added = True
break
except TypeError:
try:
getattr(lib_manager, add_name)(name)
added = True
break
except Exception as e2:
last_err = e2
except Exception as e2:
last_err = e2
if added:
updated = True
print("DEBUG: remove + re-add for %s OK" % name)
except Exception as e:
last_err = e
print("DEBUG: remove+add failed for %s: %s" % (name, e))
if updated:
results.append((name, namespace, old_version, TARGET_VERSION, "ok"))
ok += 1
else:
results.append((name, namespace, old_version, old_version, "failed: %s" % last_err))
failed += 1
except Exception as e:
results.append(("?", "?", "?", "?", "error: %s" % e))
failed += 1
try:
primary_project.save()
except Exception as save_err:
print("ERROR: save failed: %s" % save_err)
print("SCRIPT_ERROR: save failed after updates: %s" % save_err)
sys.exit(1)
print("Library update summary:")
print(" total=%d updated=%d skipped=%d failed=%d" % (len(refs), ok, skipped, failed))
print(" target version: %s" % TARGET_VERSION)
print("Per-library:")
for name, ns, old, new, status in results:
prefix = "%s.%s" % (ns, name) if ns else name
print(" %-50s %s -> %s [%s]" % (prefix, old, new, status))
if failed > 0:
print("SCRIPT_ERROR: %d libraries failed to update; see lines above" % failed)
sys.exit(1)
print("SCRIPT_SUCCESS: %d libraries updated, %d skipped" % (ok, skipped))
sys.exit(0)
except Exception as e:
detailed = traceback.format_exc()
msg = "Error updating libraries in '%s': %s\n%s" % (PROJECT_FILE_PATH, e, detailed)
print(msg)
print("SCRIPT_ERROR: %s" % msg)
sys.exit(1)

View file

@ -1180,6 +1180,35 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
}
);
s.tool(
'update_all_libraries',
"Updates every library reference in a project's Library Manager to a target version (default '*' = always-newest installed). By default skips system-pinned references; set includeSystem=true to also rewrite those (risky for device-tied projects). Saves the project on success.",
{
projectFilePath: z.string().describe("Path to the project file."),
targetVersion: z.string().optional().describe("Version to set for each library reference. '*' (default) means always-newest installed; otherwise an exact version string like '3.5.20.0'."),
includeSystem: z.boolean().optional().describe("If true, also update libraries flagged as system. Default: false. Be careful -- changing system library versions can break device-tied projects."),
},
async (args: { projectFilePath: string; targetVersion?: string; includeSystem?: boolean }) => {
const escaped = resolvePath(args.projectFilePath, workspaceDir);
const target = (args.targetVersion ?? '*').trim();
const include = args.includeSystem === true;
const script = scriptManager.prepareScriptWithHelpers(
'update_all_libraries',
{
PROJECT_FILE_PATH: escaped,
TARGET_VERSION: target,
INCLUDE_SYSTEM: include ? 'True' : 'False',
},
['ensure_project_open']
);
const result = await executor.executeScript(script);
return formatToolResponse(
result,
`Library references updated to '${target}' in ${args.projectFilePath}. Project saved.`
);
}
);
s.tool(
'install_library_from_url',
"Downloads a .library file from a URL (HTTPS supported, follows redirects) and installs it into the CODESYS Library Repository. Companion to install_library_file for fully-automated bring-up from internal shares, vendor download URLs, or GitHub release assets. Does not need a project to be open.",