feat: add install_library_file MCP tool for system-wide library installs
Adds a new tool that installs a .library file into the CODESYS Library
Repository via the scriptengine API, independent of any open project.
Useful for automating library bring-up on a fresh machine when the
CODESYS Store auto-download is broken (the immediate motivation: WAGO
libraries 404 against store.codesys.com / store-archive.codesys.com).
- src/scripts/install_library_file.py:
Single LIBRARY_FILE_PATH parameter. Defensive multi-pattern API
probe -- libraries can be reached via either script_engine.libraries
or script_engine.system.libraries depending on SP19/SP21/SP22, and
the install method may be install_library / install / add_library /
add with either (path,) or (path, overwrite) signatures. Picks the
first writable (non-system) repository it finds. Reports installed
name + version + repository on success.
- src/server.ts:
Wires the script as MCP tool 'install_library_file' immediately
after add_library. Does NOT use the ensure_project_open helper,
because repository installs work without a project loaded.
ASCII-only source (per the IronPython 2.7 lesson from 0.4.1).
Verification pending -- needs a real .library file to install.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3bdd48f93a
commit
1f23cb0498
2 changed files with 153 additions and 0 deletions
133
src/scripts/install_library_file.py
Normal file
133
src/scripts/install_library_file.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import sys, scriptengine as script_engine, os, traceback
|
||||
|
||||
LIBRARY_FILE_PATH = r"{LIBRARY_FILE_PATH}"
|
||||
|
||||
try:
|
||||
print("DEBUG: install_library_file: File='%s'" % LIBRARY_FILE_PATH)
|
||||
if not LIBRARY_FILE_PATH:
|
||||
raise ValueError("Library file path is empty.")
|
||||
if not os.path.exists(LIBRARY_FILE_PATH):
|
||||
raise IOError("Library file not found: %s" % LIBRARY_FILE_PATH)
|
||||
|
||||
# Locate a writable Library Repository handle.
|
||||
# CODESYS scripting exposes libraries either as a top-level alias
|
||||
# (script_engine.libraries) or under the system module
|
||||
# (script_engine.system.libraries). Both shapes have been observed
|
||||
# across SP19/SP21/SP22.
|
||||
repo = None
|
||||
repo_name = "?"
|
||||
|
||||
candidates = []
|
||||
try:
|
||||
if hasattr(script_engine, 'libraries'):
|
||||
candidates.append(("script_engine.libraries", script_engine.libraries))
|
||||
except Exception as e:
|
||||
print("DEBUG: probing script_engine.libraries failed: %s" % e)
|
||||
try:
|
||||
sys_mod = getattr(script_engine, 'system', None)
|
||||
if sys_mod is not None and hasattr(sys_mod, 'libraries'):
|
||||
candidates.append(("script_engine.system.libraries", sys_mod.libraries))
|
||||
except Exception as e:
|
||||
print("DEBUG: probing script_engine.system.libraries failed: %s" % e)
|
||||
|
||||
for src_name, libs in candidates:
|
||||
print("DEBUG: candidate %s -> %s" % (src_name, type(libs).__name__))
|
||||
# Case A: object exposes install_library() directly (acts as a single repo)
|
||||
if hasattr(libs, 'install_library') or hasattr(libs, 'install'):
|
||||
repo = libs
|
||||
repo_name = src_name
|
||||
break
|
||||
# Case B: object exposes a 'repositories' collection. Pick the first
|
||||
# non-system, writable-looking repository.
|
||||
if hasattr(libs, 'repositories'):
|
||||
try:
|
||||
rlist = list(libs.repositories)
|
||||
except Exception:
|
||||
rlist = []
|
||||
print("DEBUG: %s.repositories -> %d repos" % (src_name, len(rlist)))
|
||||
chosen = None
|
||||
for r in rlist:
|
||||
is_sys = False
|
||||
try:
|
||||
flag = getattr(r, 'is_system', None)
|
||||
if callable(flag):
|
||||
is_sys = bool(flag())
|
||||
elif flag is not None:
|
||||
is_sys = bool(flag)
|
||||
except Exception:
|
||||
pass
|
||||
if not is_sys:
|
||||
chosen = r
|
||||
break
|
||||
if chosen is None and rlist:
|
||||
chosen = rlist[0]
|
||||
if chosen is not None:
|
||||
repo = chosen
|
||||
try:
|
||||
nm = getattr(repo, 'get_name', None)
|
||||
repo_name = nm() if callable(nm) else getattr(repo, 'name', '?')
|
||||
except Exception:
|
||||
repo_name = "?"
|
||||
break
|
||||
|
||||
if repo is None:
|
||||
raise RuntimeError("Could not locate a Library Repository via scripting API. Tried script_engine.libraries and script_engine.system.libraries.")
|
||||
|
||||
print("DEBUG: target repository: %s" % repo_name)
|
||||
|
||||
# Try install methods in order of likelihood. Some accept (path, overwrite),
|
||||
# some accept just (path). Try both arities for each name.
|
||||
installed = None
|
||||
last_err = None
|
||||
for method_name in ('install_library', 'install', 'add_library', 'add'):
|
||||
if not hasattr(repo, method_name):
|
||||
continue
|
||||
method = getattr(repo, method_name)
|
||||
for args in ((LIBRARY_FILE_PATH,), (LIBRARY_FILE_PATH, False), (LIBRARY_FILE_PATH, True)):
|
||||
try:
|
||||
installed = method(*args)
|
||||
print("DEBUG: %s%s succeeded" % (method_name, args))
|
||||
break
|
||||
except TypeError as e:
|
||||
last_err = e
|
||||
continue
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
print("DEBUG: %s%s failed: %s" % (method_name, args, e))
|
||||
break
|
||||
if installed is not None:
|
||||
break
|
||||
|
||||
if installed is None:
|
||||
raise RuntimeError("Install failed via all known method patterns. Last error: %s" % last_err)
|
||||
|
||||
# Best-effort name/version extraction
|
||||
def _attr(obj, name, default='?'):
|
||||
try:
|
||||
v = getattr(obj, name, None)
|
||||
if callable(v):
|
||||
return v()
|
||||
if v is not None:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return default
|
||||
|
||||
lib_name = _attr(installed, 'get_name')
|
||||
if lib_name == '?':
|
||||
lib_name = _attr(installed, 'name')
|
||||
lib_version = _attr(installed, 'get_version')
|
||||
if lib_version == '?':
|
||||
lib_version = _attr(installed, 'version')
|
||||
|
||||
print("Installed: %s %s" % (lib_name, lib_version))
|
||||
print("Repository: %s" % repo_name)
|
||||
print("File: %s" % LIBRARY_FILE_PATH)
|
||||
print("SCRIPT_SUCCESS: Library installed.")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
detailed = traceback.format_exc()
|
||||
msg = "Error installing library file '%s': %s\n%s" % (LIBRARY_FILE_PATH, e, detailed)
|
||||
print(msg)
|
||||
print("SCRIPT_ERROR: %s" % msg)
|
||||
sys.exit(1)
|
||||
|
|
@ -1044,6 +1044,26 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
|
|||
}
|
||||
);
|
||||
|
||||
s.tool(
|
||||
'install_library_file',
|
||||
"Installs a .library file into the CODESYS Library Repository (system-wide). Useful for automating library bring-up on a fresh machine when the CODESYS Store auto-download is broken (e.g. WAGO libraries). Does not need a project to be open.",
|
||||
{
|
||||
libraryFilePath: z.string().describe("Full path to the .library file on disk to install."),
|
||||
},
|
||||
async (args: { libraryFilePath: string }) => {
|
||||
const escaped = resolvePath(args.libraryFilePath, workspaceDir);
|
||||
const script = scriptManager.prepareScript(
|
||||
'install_library_file',
|
||||
{ LIBRARY_FILE_PATH: escaped }
|
||||
);
|
||||
const result = await executor.executeScript(script);
|
||||
return formatToolResponse(
|
||||
result,
|
||||
`Library file installed: ${args.libraryFilePath}`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// ─── Resources ───────────────────────────────────────────────────────
|
||||
|
||||
server.resource(
|
||||
|
|
|
|||
Loading…
Reference in a new issue