0
0
Fork 0

list_project_libraries: capture + render project version + IDE + devices

Enriches the tool's output with project-level metadata that previously
lived only in hand-edited library.md headers:

  Project info:
    Version: 1.0.0.0          (Project Information.version)
    Title:   ...              (Project Information.title)
    Company: ...              (Project Information.company)
    Author:  ...              (Project Information.author)
  IDE:    CODESYS V3.5 SP22 Patch 1, ScriptEngine.plugin 4.2.0.0
  Devices (N):
    MainPLC                   [4096 / 1006 120D / 6.2.0.1]
    MainPLC/Kbus              [32778 / Wago 750-Series Local Bus Interface / 2.1.0.1]
    ...

Implementation:
  - collect_project_info() reads .version / .title / .company / .author
    on the Project Information node (first child of project root). Each
    field is read defensively (try/except) since some installs leave
    them unset; missing fields are dropped from the output.
  - collect_devices() walks the tree depth-first for nodes where
    is_device is True, captures get_device_identification() into a
    type/id/version triple. The triple is the offline target id the
    IDE uses to pick a compiler + runtime when building -- not the
    live firmware reported by a connected PLC over a runtime
    connection (the latter would require an online connect).
  - sys.version inside IronPython under CODESYS reports the IDE
    version directly (same string we see in ready.signal).

server.ts renders these as a Header block above the existing
library-by-container tables. Hand-edited X33/library.md "Versions"
section is now redundant -- next regeneration will produce the
header automatically.

Verified via the local SP22 install + the live X33 watcher: pi.version
read-back works after bump_project_version sets it, devices walk
returns 13 entries on X33 (MainPLC + 11 Kbus modules + the network
adapter).
This commit is contained in:
Karstein Phobic Nyvold Kvistad 2026-04-26 00:12:06 +02:00
parent 75d77e2fb2
commit e37a2191a9
2 changed files with 123 additions and 4 deletions

View file

@ -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,
}

View file

@ -1202,7 +1202,7 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
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<void> {
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<void> {
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) {