diff --git a/src/scripts/list_project_libraries.py b/src/scripts/list_project_libraries.py index 121df63..87665b4 100644 --- a/src/scripts/list_project_libraries.py +++ b/src/scripts/list_project_libraries.py @@ -78,16 +78,99 @@ def reference_to_dict(ref): return entry +def collect_project_info(project): + """Read the Project Information node (first child of project root): + version, title, company, author. Used to surface the project version + above the library list in the rendered markdown.""" + info = {'version': None, 'title': None, 'company': None, 'author': None} + try: + for child in project.get_children(False): + try: + if child.get_name() != 'Project Information': + continue + except Exception: + continue + for attr in ('version', 'title', 'company', 'author'): + try: + v = getattr(child, attr, None) + if v is None: + continue + s = str(v) + if s and s != 'None': + info[attr] = s + except Exception: + pass + break + except Exception: + pass + return info + + +def collect_devices(project): + """Walk the tree and capture every is_device=True node's get_device_identification() + (the offline target id from project settings: type/id/version). Lets the + markdown renderer show 'MainPLC target firmware version' alongside the + library list.""" + devices = [] + + def walk(node, prefix='', depth=0, max_depth=10): + if depth > max_depth: + return + try: + name = node.get_name() + except Exception: + name = '?' + path = (prefix + '/' + name) if prefix else name + is_dev = False + try: + is_dev = bool(node.is_device) + except Exception: + pass + if is_dev: + entry = {'path': path, 'name': name} + try: + ident = node.get_device_identification() + for attr in ('type', 'id', 'version'): + v = getattr(ident, attr, None) + if v is not None: + entry['device_id_' + attr] = str(v) + except Exception: + pass + devices.append(entry) + try: + children = node.get_children(False) + except Exception: + children = [] + for c in children: + walk(c, path, depth + 1, max_depth) + + try: + for child in project.get_children(False): + walk(child) + except Exception: + pass + return devices + + 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) + project_info = collect_project_info(primary_project) + devices = collect_devices(primary_project) + ide_version = sys.version # IronPython under CODESYS reports the IDE + print("DEBUG: project_info: %s" % project_info) + print("DEBUG: %d device(s) found." % len(devices)) + containers = find_libman_containers(primary_project) print("DEBUG: %d libman container(s) found in tree." % len(containers)) result = { 'project': project_basename, + 'project_info': project_info, + 'ide_version': ide_version, + 'devices': devices, 'containers': [], 'total_references': 0, } diff --git a/src/server.ts b/src/server.ts index 54a6722..4697b83 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 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.", + "Lists every library referenced anywhere in the CODESYS project AND captures project-level metadata above the library list: Project Information (version, title, company, author), CODESYS Development System version (from IronPython sys.version), and every device's offline target identification triple (type / id / version) -- the offline 'firmware' the project is built against. The library list itself walks the project tree, finds every ScriptLibManObjectContainer (the project + 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). Per the helpme-codesys.com docs and the local SP22 stub Stubs/scriptengine/ScriptLibManObject.pyi.", { projectFilePath: z.string().describe("Path to the project file."), }, @@ -1250,8 +1250,16 @@ export async function startMcpServer(config: ServerConfig): Promise { source?: string; }; type Container = { container_name: string; libman_name: string; references: LibRef[] }; - const parsed: { project?: string; containers: Container[]; total_references: number } = - JSON.parse(jsonStr); + type ProjectInfo = { version?: string | null; title?: string | null; company?: string | null; author?: string | null }; + type Device = { path: string; name?: string; device_id_type?: string; device_id_id?: string; device_id_version?: string }; + const parsed: { + project?: string; + project_info?: ProjectInfo; + ide_version?: string; + devices?: Device[]; + containers: Container[]; + total_references: number; + } = JSON.parse(jsonStr); if (!parsed.containers || parsed.containers.length === 0) { return { @@ -1301,10 +1309,38 @@ export async function startMcpServer(config: ServerConfig): Promise { sections.push(`${header}\n${lines.join('\n')}`); } + // Header section: project version (from Project Information), + // CODESYS Development System version (from IronPython sys.version + // inside CODESYS), and every device's offline target id triple. + const headerLines: string[] = []; + const pi = parsed.project_info ?? {}; + if (pi.version || pi.title || pi.company) { + headerLines.push('Project info:'); + if (pi.version) headerLines.push(` Version: ${pi.version}`); + if (pi.title) headerLines.push(` Title: ${pi.title}`); + if (pi.company) headerLines.push(` Company: ${pi.company}`); + if (pi.author) headerLines.push(` Author: ${pi.author}`); + } + if (parsed.ide_version) { + headerLines.push(`IDE: ${parsed.ide_version.replace(/\s+/g, ' ').trim()}`); + } + if (parsed.devices && parsed.devices.length > 0) { + headerLines.push(`Devices (${parsed.devices.length}):`); + for (const d of parsed.devices) { + const idStr = [d.device_id_type, d.device_id_id, d.device_id_version] + .filter(Boolean) + .join(' / '); + headerLines.push(` ${d.path}${idStr ? ' [' + idStr + ']' : ''}`); + } + } + const summary = `Project: ${parsed.project ?? '?'} — ${parsed.total_references} library reference(s) across ${parsed.containers.length} container(s).`; + const blocks: string[] = [summary]; + if (headerLines.length > 0) blocks.push(headerLines.join('\n')); + blocks.push(sections.join('\n\n')); return { - content: [{ type: 'text' as const, text: `${summary}\n\n${sections.join('\n\n')}` }], + content: [{ type: 'text' as const, text: blocks.join('\n\n') }], isError: false, }; } catch (e) {