0
0
Fork 0

fix(add_library): refuse hollow placeholder when library not installed in repo

The pre-resolve via library_manager.find_library() at line 218 already
detected when the requested name was not present in the installed
library repository -- it logged "Pre-resolve... returned no hit" -- but
the script then proceeded to call add_placeholder(LIBRARY_NAME) without
a managed-lib argument anyway. CODESYS happily creates such a reference
with is_placeholder=False, so the post-add _is_resolved() guard returns
True and the project gets saved with a hollow reference. The next time
the project is opened, the IDE pops:

    Library Manager: Error: Could not open library 'X'.
    (Reason: The placeholder library 'X' could not be resolved.)

...and compile fails until the user manually deletes the bad reference
from the Library Manager.

Karstein hit this on 2026-04-28 trying to add "OPC UA Pub Sub" (the
real library is "OPC UA PubSub SL", an add-on SL package not present
in the stock V3.5 SP22 install). The script returned SCRIPT_SUCCESS,
list_project_libraries showed it as `[managed]`, and only on the next
set_pou_code call did the broken-placeholder error surface.

Fix: when _resolve_in_repo returns None, hard-refuse upfront with a
clear error pointing at the Library Repository / CODESYS Installer.
Opt-in via ALLOW_UNRESOLVED=1 (mapped to the new MCP arg
`allowUnresolved: true`) for the rare case where a placeholder for a
not-yet-installed library is genuinely wanted.

Tool description and arg docs updated to mark allowUnresolved as
DANGEROUS so future agent calls don't reach for it casually.

Bumped 0.6.0 -> 0.6.1.
This commit is contained in:
Karstein Phobic Nyvold Kvistad 2026-04-28 23:05:32 +02:00
parent fec74e8943
commit 3e9f4b8403
3 changed files with 28 additions and 4 deletions

View file

@ -1,6 +1,6 @@
{
"name": "codesys-mcp-sp21-plus",
"version": "0.6.0",
"version": "0.6.1",
"description": "Codesys-MCP-SP21+ -- fork of luke-harriman/Codesys-MCP carrying CODESYS V3.5 SP22 Patch 1 fixes (and forward-compat with later SPs): script-engine API drift, online/runtime tool auto-login, dual-SHA release classifier, set_pou_code omitted-decl wipe fix, add_library managed-overload, etc. MCP server for CODESYS with persistent UI instance and file-based IPC.",
"main": "dist/server.js",
"bin": {

View file

@ -43,6 +43,7 @@ import sys, scriptengine as script_engine, os, traceback
LIBRARY_NAME = "{LIBRARY_NAME}"
USE_DIRECT = "{USE_DIRECT}" == "1"
FORCE_DUP = "{FORCE_DUP}" == "1"
ALLOW_UNRESOLVED = "{ALLOW_UNRESOLVED}" == "1"
def _resolve_in_repo(name):
@ -224,6 +225,27 @@ try:
print("DEBUG: Pre-resolved '%s' to installed library '%s'." % (LIBRARY_NAME, disp))
else:
print("DEBUG: Pre-resolve via library_manager.find_library returned no hit for '%s'." % LIBRARY_NAME)
# HARD REFUSE: if the name does not resolve in the installed
# library repository, do NOT add anything. The post-add
# _is_resolved() guard alone is insufficient -- CODESYS lets
# add_placeholder(name_string) create a reference where
# is_placeholder=False but the library is still unresolvable on
# next project open, bricking compile with
# "The placeholder library '<name>' could not be resolved."
# Opt-in via ALLOW_UNRESOLVED=1 for the rare case where a
# placeholder for a not-yet-installed library is genuinely wanted.
if not ALLOW_UNRESOLVED:
msg = ("Refused: library '%s' is not installed in the CODESYS library "
"repository (library_manager.find_library returned no hit). "
"Install it via the Library Repository (Tools > Library Repository, "
"or via CODESYS Installer for SL/add-on packages) before re-running, "
"or pass allowUnresolved=true if you really want a placeholder for "
"a not-yet-installed library."
% LIBRARY_NAME)
print("ERROR: %s" % msg)
print("SCRIPT_ERROR: %s" % msg)
sys.exit(1)
print("DEBUG: ALLOW_UNRESOLVED=1 -- proceeding with placeholder add despite no repo hit.")
# Step 2: add the reference. Default is add_placeholder() to match the
# modern '<Name>, * (System)' convention (placeholder resolves at

View file

@ -1871,14 +1871,15 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
s.tool(
'add_library',
"Adds a library reference to the CODESYS project. By default uses add_placeholder() to match the modern '<Name>, * (System)' convention so transitive deps resolve at compile time -- pass direct=true to opt into the legacy direct add_library() (specific version pin). Pre-checks lm.references for an existing reference with the same name and no-ops with a confirmation message unless force=true. The library must be installed in the CODESYS library repository.",
"Adds a library reference to the CODESYS project. **Refuses upfront** if the library name does not resolve in the installed library repository (Tools > Library Repository) -- this prevents the silent-broken-placeholder bug where add_placeholder() creates a hollow reference that bricks the next project open with 'placeholder library X could not be resolved'. Install the library first (Library Repository for stock libs, CODESYS Installer for SL/add-on packages), or pass allowUnresolved=true to opt into the dangerous behaviour. Default add path is add_placeholder() for the modern '<Name>, * (System)' convention; pass direct=true for the legacy specific-version pin. Pre-checks lm.references and no-ops with a confirmation message if a reference with the same name already exists, unless force=true.",
{
projectFilePath: z.string().describe("Path to the project file."),
libraryName: z.string().describe("Name of the library to add (e.g., 'Standard', 'Util', 'CAA Memory')."),
libraryName: z.string().describe("Name of the library to add (e.g., 'Standard', 'Util', 'CAA Memory'). Must match exactly an installed library in the CODESYS Library Repository unless allowUnresolved=true."),
direct: z.boolean().optional().describe("If true, use direct add_library() (specific-version pin) instead of the default add_placeholder() (resolves at compile)."),
force: z.boolean().optional().describe("If true, add even if a reference with the same name already exists (creates a duplicate). Default: dedup -- silently no-op with a confirmation message."),
allowUnresolved: z.boolean().optional().describe("DANGEROUS. If true, skip the pre-flight 'is this library installed?' check and add a placeholder anyway. Will brick the next project open if the library is genuinely not installed. Only use when you intentionally want a placeholder for a not-yet-installed library."),
},
async (args: { projectFilePath: string; libraryName: string; direct?: boolean; force?: boolean }) => {
async (args: { projectFilePath: string; libraryName: string; direct?: boolean; force?: boolean; allowUnresolved?: boolean }) => {
const escaped = resolvePath(args.projectFilePath, workspaceDir);
const script = scriptManager.prepareScriptWithHelpers(
'add_library',
@ -1887,6 +1888,7 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
LIBRARY_NAME: args.libraryName.trim(),
USE_DIRECT: args.direct ? '1' : '0',
FORCE_DUP: args.force ? '1' : '0',
ALLOW_UNRESOLVED: args.allowUnresolved ? '1' : '0',
},
['ensure_project_open']
);