0
0
Fork 0

fix(list_project_libraries): use ScriptLibManObjectContainer API

RTFM. Per the helpme-codesys.com Library Manager scripting page and
the local SP22 stub Stubs/scriptengine/ScriptLibManObject.pyi:

- ScriptLibManObjectContainer is added to BOTH the project AND every
  Application object. It exposes:
    has_library_manager   -- @property (NOT a method) returning bool
    get_library_manager() -- method returning the LibMan ScriptObject
- ScriptLibManObject (the libman itself) exposes:
    .references            -- @property, ScriptLibraryReferences (list-
                              like) of ScriptLibraryReference objects
                              with structured fields (name, namespace,
                              is_placeholder, is_managed, system_library,
                              effective_resolution, ...)
    get_libraries(recursive=False)  -- list[str], names only
- ScriptLibManObjectMarker.is_libman is the universal marker, useful
  as a fallback when walking the tree.

The previous implementation searched the project tree by NAME for an
object literally called "Library Manager" via primary_project.find()
and a children name probe. That never matches because the libman's
actual name is generated, not "Library Manager." On the X33 project
(MRCodesysX33_0021) it returned empty -- false negative on a project
that obviously has dozens of system + application libraries. This
also took add_library down with it (same wrong axis), and the smoke
test from earlier in the fork's history flagged the inconsistency
without identifying the cause.

Fixed:
- Walk the tree depth-first, accept any node where has_library_manager
  is True (depth-limited at 8 to be safe).
- For each, call get_library_manager() and iterate .references for
  structured data; fall back to get_libraries() name-only enumeration
  if .references is unavailable on the SP.
- Capture every documented ScriptLibraryReference field defensively
  (each access wrapped in try/except since some fields raise on
  placeholders / unmanaged / SP-version skew).
- Return a structured JSON shape grouped by container so the TS side
  can show which Application owns which libraries.

server.ts:
- Updated the result-parsing block to handle the new structured shape.
- Distinguishes "no library managers found" (suspicious, libman
  discovery probably broken) from "found managers, all empty" (just
  empty applications).
- Renders flags ([system, placeholder, managed, optional, redirected])
  + namespace + effective_resolution per reference.
- Tool description rewritten to advertise the actual mechanism and
  cite the doc + local stub source.

add_library is NOT fixed in this commit -- it has the same wrong-axis
bug but landing the lookup-only fix first to verify the API contract.
add_library will land separately once we know list_project_libraries
sees the right libman in production (X33 smoke test).
This commit is contained in:
Karstein Phobic Nyvold Kvistad 2026-04-25 21:59:23 +02:00
parent 95a884bf1e
commit 9b766c8b6b
2 changed files with 209 additions and 77 deletions

View file

@ -1,82 +1,155 @@
import sys, scriptengine as script_engine, os, traceback, json
try:
print("DEBUG: list_project_libraries script: Project='%s'" % PROJECT_FILE_PATH)
primary_project = ensure_project_open(PROJECT_FILE_PATH)
project_name = os.path.basename(PROJECT_FILE_PATH)
# RTFM (helpme-codesys.com + local SP22 stub
# Stubs/scriptengine/ScriptLibManObject.pyi):
#
# - ScriptLibManObjectContainer is a marker interface added to BOTH the
# project AND every Application object. Two key members:
# has_library_manager -- @property (NOT a method) returning bool
# get_library_manager() -- method, returns the LibMan ScriptObject
# - ScriptLibManObject (the LibMan itself) exposes:
# .references -- @property, ScriptLibraryReferences (list-like) of
# ScriptLibraryReference. Best for structured data.
# get_libraries(recursive=False) -- list[str] of library names.
# - ScriptLibManObjectMarker.is_libman is the universal "is this a libman?"
# property added to every ScriptObject, useful as a fallback when walking
# the project tree.
#
# The previous version of this script searched the tree for an object whose
# NAME matched "Library Manager" (via find() / get_children name probe). That
# never worked because the libman's actual name is generated, not literal.
# Fixed by walking every container with has_library_manager and pulling
# references via lm.references.
libraries = []
lib_manager = None
# Find Library Manager object
# Pattern 1: Search for it by name in project tree
def find_libman_containers(node, depth=0, max_depth=8):
"""Walk the project tree and yield every node where has_library_manager
is True. Depth-limited so a malformed project tree can't loop us."""
out = []
if depth > max_depth:
return out
try:
found_list = primary_project.find("Library Manager", True)
if found_list:
lib_manager = found_list[0]
print("DEBUG: Found Library Manager via find('Library Manager')")
except Exception as e:
print("DEBUG: find('Library Manager') failed: %s" % e)
# has_library_manager is a property, not a method -- accessing it
# on a non-container ScriptObject can raise, so guard.
if hasattr(node, 'has_library_manager'):
try:
hlm = node.has_library_manager
except Exception:
hlm = False
if hlm:
out.append(node)
except Exception:
pass
try:
children = node.get_children(False)
except Exception:
children = []
for child in children:
out.extend(find_libman_containers(child, depth + 1, max_depth))
return out
# Pattern 2: Search all children
if not lib_manager:
def safe_get(obj, attr, default=None):
"""getattr that swallows access exceptions (some properties throw on
placeholders / unmanaged refs depending on the SP). Returns default
if missing or raising; calls callables."""
try:
if not hasattr(obj, attr):
return default
v = getattr(obj, attr)
return v() if callable(v) else v
except Exception:
return default
def reference_to_dict(ref):
"""Capture as much structured info as the SP exposes, defensively."""
entry = {}
for prop in ('id', 'name', 'namespace', 'is_placeholder', 'is_managed',
'system_library', 'qualified_only', 'optional',
'placeholder_name', 'effective_resolution',
'default_resolution', 'is_redirected', 'resolution_info'):
v = safe_get(ref, prop)
if v is not None:
try:
entry[prop] = str(v) if not isinstance(v, bool) else v
except Exception:
pass
return entry
try:
print("DEBUG: list_project_libraries: Project='%s'" % PROJECT_FILE_PATH)
primary_project = ensure_project_open(PROJECT_FILE_PATH)
project_basename = os.path.basename(PROJECT_FILE_PATH)
containers = find_libman_containers(primary_project)
print("DEBUG: %d libman container(s) found in tree." % len(containers))
result = {
'project': project_basename,
'containers': [],
'total_references': 0,
}
for container in containers:
container_name = safe_get(container, 'get_name', '?')
try:
all_children = primary_project.get_children(True)
for child in all_children:
child_name = getattr(child, 'get_name', lambda: '')()
if 'library' in child_name.lower() and 'manager' in child_name.lower():
lib_manager = child
print("DEBUG: Found Library Manager by name search: %s" % child_name)
break
lm = container.get_library_manager()
except Exception as e:
print("DEBUG: Children search for Library Manager failed: %s" % e)
print("DEBUG: get_library_manager() failed on '%s': %s" % (container_name, e))
continue
if lm is None:
print("DEBUG: container '%s' returned None for get_library_manager()." % container_name)
continue
lm_name = safe_get(lm, 'get_name', '?')
if lib_manager:
print("DEBUG: Library Manager found: %s" % getattr(lib_manager, 'get_name', lambda: '?')())
# Try to enumerate libraries
# Try the structured .references property first; fall back to the
# name-only get_libraries() method.
refs_struct = []
ref_iter = None
try:
lib_children = lib_manager.get_children(False)
for lib_child in lib_children:
lib_name = getattr(lib_child, 'get_name', lambda: '?')()
lib_entry = {'name': lib_name}
# Try to get version info
if hasattr(lib_child, 'version'):
try:
lib_entry['version'] = str(lib_child.version)
except Exception:
pass
if hasattr(lib_child, 'get_version'):
try:
lib_entry['version'] = str(lib_child.get_version())
except Exception:
pass
# Try to get company/vendor
if hasattr(lib_child, 'company'):
try:
lib_entry['company'] = str(lib_child.company)
except Exception:
pass
libraries.append(lib_entry)
print("DEBUG: Found library: %s" % lib_name)
ref_iter = lm.references
except Exception as e:
print("WARN: Error enumerating libraries: %s" % e)
else:
print("WARN: Library Manager not found in project.")
print("DEBUG: lm.references failed on '%s': %s" % (lm_name, e))
if ref_iter is not None:
try:
for r in ref_iter:
refs_struct.append(reference_to_dict(r))
except Exception as e:
print("DEBUG: iterating lm.references on '%s' failed: %s" % (lm_name, e))
# Fallback: name-only enumeration via get_libraries(recursive=False).
if not refs_struct and hasattr(lm, 'get_libraries'):
try:
names = lm.get_libraries(False)
for n in names:
refs_struct.append({'name': str(n), 'source': 'get_libraries-fallback'})
print("DEBUG: get_libraries fallback returned %d name(s) on '%s'" % (len(names), lm_name))
except Exception as e:
print("DEBUG: get_libraries() fallback failed on '%s': %s" % (lm_name, e))
result['containers'].append({
'container_name': container_name,
'libman_name': lm_name,
'references': refs_struct,
})
result['total_references'] += len(refs_struct)
print("DEBUG: container '%s' (libman '%s'): %d reference(s)" % (
container_name, lm_name, len(refs_struct)))
libs_json = json.dumps(libraries)
print("### LIBRARIES_START ###")
print(libs_json)
print(json.dumps(result))
print("### LIBRARIES_END ###")
print("Library Count: %d" % len(libraries))
print("Total references across %d container(s): %d" % (
len(result['containers']), result['total_references']))
print("SCRIPT_SUCCESS: Project libraries listed.")
sys.exit(0)
except Exception as e:
detailed_error = traceback.format_exc()
error_message = "Error listing libraries for project %s: %s\n%s" % (PROJECT_FILE_PATH, e, detailed_error)
print(error_message)
print("SCRIPT_ERROR: %s" % error_message)
detailed = traceback.format_exc()
msg = "Error in list_project_libraries for project '%s': %s\n%s" % (
PROJECT_FILE_PATH, e, detailed)
print(msg)
print("SCRIPT_ERROR: %s" % msg)
sys.exit(1)

View file

@ -1202,7 +1202,7 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
s.tool(
'list_project_libraries',
'Lists all libraries currently referenced in the CODESYS project.',
"Lists every library referenced anywhere in the CODESYS project: walks the project tree, finds every ScriptLibManObjectContainer (the project itself + each Application), gets the Library Manager via container.get_library_manager(), and enumerates lm.references for structured per-reference info (name, namespace, system/placeholder/managed flags, effective resolution). Output is grouped by container so you can see which Application owns which libraries. Per the helpme-codesys.com docs and the local SP22 stub Stubs/scriptengine/ScriptLibManObject.pyi.",
{
projectFilePath: z.string().describe("Path to the project file."),
},
@ -1233,29 +1233,88 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
try {
const jsonStr = result.output.substring(startIdx + libStartMarker.length, endIdx).trim();
const libraries: Array<{ name: string; version?: string; company?: string }> = JSON.parse(jsonStr);
type LibRef = {
id?: string;
name?: string;
namespace?: string;
is_placeholder?: boolean;
is_managed?: boolean;
system_library?: boolean;
qualified_only?: boolean;
optional?: boolean;
placeholder_name?: string;
effective_resolution?: string;
default_resolution?: string;
is_redirected?: boolean;
resolution_info?: string;
source?: string;
};
type Container = { container_name: string; libman_name: string; references: LibRef[] };
const parsed: { project?: string; containers: Container[]; total_references: number } =
JSON.parse(jsonStr);
if (libraries.length === 0) {
if (!parsed.containers || parsed.containers.length === 0) {
return {
content: [{ type: 'text' as const, text: 'No libraries found in the project (or Library Manager not found).' }],
content: [
{
type: 'text' as const,
text:
'No library managers found in the project tree. ' +
'Either the project really has none, or the libman discovery failed -- ' +
'check the script DEBUG output for a tree dump.',
},
],
isError: false,
};
}
const lines = libraries.map((lib) => {
let line = `- ${lib.name}`;
if (lib.version) line += ` (v${lib.version})`;
if (lib.company) line += ` [${lib.company}]`;
return line;
});
if (parsed.total_references === 0) {
const containerNames = parsed.containers.map((c) => c.container_name).join(', ');
return {
content: [
{
type: 'text' as const,
text: `Found ${parsed.containers.length} library manager(s) (${containerNames}) but 0 library references in any of them.`,
},
],
isError: false,
};
}
// Group output by container so the user can see which Application
// owns which libraries.
const sections: string[] = [];
for (const c of parsed.containers) {
const header = `${c.container_name} (libman: ${c.libman_name}) — ${c.references.length} reference(s)`;
const lines = c.references.map((ref) => {
const flags: string[] = [];
if (ref.system_library) flags.push('system');
if (ref.is_placeholder) flags.push('placeholder');
if (ref.is_managed) flags.push('managed');
if (ref.optional) flags.push('optional');
if (ref.is_redirected) flags.push('redirected');
const flagStr = flags.length > 0 ? ` [${flags.join(', ')}]` : '';
const ns = ref.namespace ? ` ns=${ref.namespace}` : '';
const eff = ref.effective_resolution ? ` -> ${ref.effective_resolution}` : '';
return ` - ${ref.name ?? '?'}${flagStr}${ns}${eff}`;
});
sections.push(`${header}\n${lines.join('\n')}`);
}
const summary =
`Project: ${parsed.project ?? '?'}${parsed.total_references} library reference(s) across ${parsed.containers.length} container(s).`;
return {
content: [{ type: 'text' as const, text: `${libraries.length} library/libraries:\n${lines.join('\n')}` }],
content: [{ type: 'text' as const, text: `${summary}\n\n${sections.join('\n\n')}` }],
isError: false,
};
} catch {
} catch (e) {
return {
content: [{ type: 'text' as const, text: 'Failed to parse libraries JSON.' }],
content: [
{
type: 'text' as const,
text: `Failed to parse libraries JSON: ${e instanceof Error ? e.message : String(e)}`,
},
],
isError: true,
};
}