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:
parent
95a884bf1e
commit
9b766c8b6b
2 changed files with 209 additions and 77 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue