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:
parent
75d77e2fb2
commit
e37a2191a9
2 changed files with 123 additions and 4 deletions
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue