diff --git a/src/scripts/list_project_libraries.py b/src/scripts/list_project_libraries.py index 429df1e..121df63 100644 --- a/src/scripts/list_project_libraries.py +++ b/src/scripts/list_project_libraries.py @@ -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) diff --git a/src/server.ts b/src/server.ts index 9db61f8..873761c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1202,7 +1202,7 @@ export async function startMcpServer(config: ServerConfig): Promise { 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 { 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, }; }