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:
parent
fec74e8943
commit
3e9f4b8403
3 changed files with 28 additions and 4 deletions
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue