feat: add set_library_version MCP tool for surgical per-library pins
Companion to update_all_libraries (9a4e96f). Where update_all_libraries
rewrites every reference in one shot, this tool targets a single library
by name -- intended for surgical fixes where most refs are fine but one
is broken. Concrete use case: X33's StringUtils pinned to 3.5.18.0
(non-resolvable) needs to move to 3.5.20.0 specifically without touching
the dozens of other references.
- src/scripts/set_library_version.py:
Same defensive multi-pattern enumeration + setter probing as
update_all_libraries. Looks up the target ref by case-insensitive
bare name match, or 'Namespace.Name' if the user disambiguates.
Returns ValueError listing all available names if no match, and a
separate ValueError listing namespace.name pairs if the bare name
matches more than one ref. Falls back to remove + re-add when the
Library Manager exposes no direct version setter on the ref.
- src/server.ts:
Wires as 'set_library_version' with required projectFilePath,
libraryName, and targetVersion. Saves the project on success.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9a4e96f11a
commit
3832f9c5cc
2 changed files with 196 additions and 0 deletions
169
src/scripts/set_library_version.py
Normal file
169
src/scripts/set_library_version.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import sys, scriptengine as script_engine, os, traceback
|
||||
|
||||
LIBRARY_NAME = "{LIBRARY_NAME}"
|
||||
TARGET_VERSION = "{TARGET_VERSION}"
|
||||
|
||||
try:
|
||||
print("DEBUG: set_library_version: Library='%s', TargetVersion='%s', Project='%s'" % (
|
||||
LIBRARY_NAME, TARGET_VERSION, PROJECT_FILE_PATH))
|
||||
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
||||
if not LIBRARY_NAME:
|
||||
raise ValueError("Library name is empty.")
|
||||
if not TARGET_VERSION:
|
||||
raise ValueError("Target version is empty.")
|
||||
|
||||
# Locate Library Manager.
|
||||
lib_manager = None
|
||||
try:
|
||||
found = primary_project.find("Library Manager", True)
|
||||
if found:
|
||||
lib_manager = found[0]
|
||||
except Exception as e:
|
||||
print("DEBUG: find('Library Manager') failed: %s" % e)
|
||||
if lib_manager is None:
|
||||
try:
|
||||
for child in primary_project.get_children(True):
|
||||
nm = getattr(child, 'get_name', lambda: '')()
|
||||
if 'library' in nm.lower() and 'manager' in nm.lower():
|
||||
lib_manager = child
|
||||
break
|
||||
except Exception as e:
|
||||
print("DEBUG: child search for Library Manager failed: %s" % e)
|
||||
if lib_manager is None:
|
||||
raise RuntimeError("Library Manager not found in project.")
|
||||
|
||||
# Enumerate refs.
|
||||
refs = []
|
||||
for method_name in ('get_all_libraries', 'get_libraries', 'get_references', 'libraries', 'references'):
|
||||
attr = getattr(lib_manager, method_name, None)
|
||||
if attr is None:
|
||||
continue
|
||||
try:
|
||||
value = attr() if callable(attr) else attr
|
||||
refs = list(value)
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if not refs:
|
||||
try:
|
||||
refs = list(lib_manager)
|
||||
except Exception:
|
||||
pass
|
||||
if not refs:
|
||||
raise RuntimeError("Could not enumerate library references via any known API.")
|
||||
|
||||
def _attr(obj, name, default=''):
|
||||
v = getattr(obj, name, None)
|
||||
if v is None:
|
||||
return default
|
||||
try:
|
||||
return v() if callable(v) else v
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
# Match by name (case-insensitive). Accept either bare name match or
|
||||
# 'namespace.name' convention.
|
||||
target_lower = LIBRARY_NAME.lower()
|
||||
matches = []
|
||||
for ref in refs:
|
||||
nm = _attr(ref, 'get_name', '')
|
||||
if not nm:
|
||||
nm = _attr(ref, 'name', '')
|
||||
ns = _attr(ref, 'get_namespace', '')
|
||||
if not ns:
|
||||
ns = _attr(ref, 'namespace', '')
|
||||
full = ("%s.%s" % (ns, nm)) if ns else nm
|
||||
if nm.lower() == target_lower or full.lower() == target_lower:
|
||||
matches.append((ref, nm, ns))
|
||||
|
||||
if not matches:
|
||||
names_seen = []
|
||||
for ref in refs:
|
||||
n = _attr(ref, 'get_name', '?')
|
||||
if not n:
|
||||
n = _attr(ref, 'name', '?')
|
||||
names_seen.append(n)
|
||||
raise ValueError(
|
||||
"No library reference matches '%s'. Found: %s"
|
||||
% (LIBRARY_NAME, ", ".join(names_seen) or "(none)")
|
||||
)
|
||||
|
||||
if len(matches) > 1:
|
||||
details = ["%s.%s" % (ns or "", nm) for _, nm, ns in matches]
|
||||
raise ValueError(
|
||||
"Library name '%s' is ambiguous; matches %d references: %s. "
|
||||
"Use 'Namespace.Name' to disambiguate."
|
||||
% (LIBRARY_NAME, len(matches), ", ".join(details))
|
||||
)
|
||||
|
||||
ref, name, namespace = matches[0]
|
||||
old_version = str(_attr(ref, 'get_version', '?'))
|
||||
if old_version == '?':
|
||||
old_version = str(_attr(ref, 'version', '?'))
|
||||
print("DEBUG: matched %s.%s @ %s" % (namespace, name, old_version))
|
||||
|
||||
# Try in-place setters.
|
||||
updated = False
|
||||
last_err = None
|
||||
for method_name in ('set_version', 'update_to_version', 'update', 'set_resolution'):
|
||||
if not hasattr(ref, method_name):
|
||||
continue
|
||||
try:
|
||||
getattr(ref, method_name)(TARGET_VERSION)
|
||||
updated = True
|
||||
print("DEBUG: %s('%s') OK" % (method_name, TARGET_VERSION))
|
||||
break
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
print("DEBUG: %s('%s') failed: %s" % (method_name, TARGET_VERSION, e))
|
||||
|
||||
# Fallback: remove + re-add at the new version.
|
||||
if not updated:
|
||||
try:
|
||||
if hasattr(lib_manager, 'remove_library'):
|
||||
lib_manager.remove_library(ref)
|
||||
elif hasattr(lib_manager, 'remove'):
|
||||
lib_manager.remove(ref)
|
||||
else:
|
||||
raise RuntimeError("no remove_library / remove method")
|
||||
for add_name in ('add_placeholder_library', 'add_library', 'insert_library'):
|
||||
if not hasattr(lib_manager, add_name):
|
||||
continue
|
||||
try:
|
||||
getattr(lib_manager, add_name)(name, TARGET_VERSION)
|
||||
updated = True
|
||||
break
|
||||
except TypeError:
|
||||
try:
|
||||
getattr(lib_manager, add_name)(name)
|
||||
updated = True
|
||||
break
|
||||
except Exception as e2:
|
||||
last_err = e2
|
||||
except Exception as e2:
|
||||
last_err = e2
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
print("DEBUG: remove+add failed for %s: %s" % (name, e))
|
||||
|
||||
if not updated:
|
||||
raise RuntimeError("Could not set version. Last error: %s" % last_err)
|
||||
|
||||
try:
|
||||
primary_project.save()
|
||||
except Exception as save_err:
|
||||
print("ERROR: save failed: %s" % save_err)
|
||||
print("SCRIPT_ERROR: save failed after setting version: %s" % save_err)
|
||||
sys.exit(1)
|
||||
|
||||
print("Library version set:")
|
||||
print(" %s.%s : %s -> %s" % (namespace, name, old_version, TARGET_VERSION))
|
||||
print("SCRIPT_SUCCESS: Library version updated.")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
detailed = traceback.format_exc()
|
||||
msg = "Error setting library version for '%s' in '%s': %s\n%s" % (
|
||||
LIBRARY_NAME, PROJECT_FILE_PATH, e, detailed)
|
||||
print(msg)
|
||||
print("SCRIPT_ERROR: %s" % msg)
|
||||
sys.exit(1)
|
||||
|
|
@ -1180,6 +1180,33 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
|
|||
}
|
||||
);
|
||||
|
||||
s.tool(
|
||||
'set_library_version',
|
||||
"Sets the version of a single library reference in the project's Library Manager. Use for surgical fixes (e.g. unpin StringUtils 3.5.18.0 -> 3.5.20.0) without touching other refs. Use update_all_libraries for bulk changes.",
|
||||
{
|
||||
projectFilePath: z.string().describe("Path to the project file."),
|
||||
libraryName: z.string().describe("Library name. Bare name matches case-insensitively (e.g. 'StringUtils'). Use 'Namespace.Name' (e.g. 'System.StringUtils') if the bare name is ambiguous."),
|
||||
targetVersion: z.string().describe("Version to set ('*' for always-newest, otherwise an exact version like '3.5.20.0')."),
|
||||
},
|
||||
async (args: { projectFilePath: string; libraryName: string; targetVersion: string }) => {
|
||||
const escaped = resolvePath(args.projectFilePath, workspaceDir);
|
||||
const script = scriptManager.prepareScriptWithHelpers(
|
||||
'set_library_version',
|
||||
{
|
||||
PROJECT_FILE_PATH: escaped,
|
||||
LIBRARY_NAME: args.libraryName.trim(),
|
||||
TARGET_VERSION: args.targetVersion.trim(),
|
||||
},
|
||||
['ensure_project_open']
|
||||
);
|
||||
const result = await executor.executeScript(script);
|
||||
return formatToolResponse(
|
||||
result,
|
||||
`Library '${args.libraryName}' set to version '${args.targetVersion}' in ${args.projectFilePath}. Project saved.`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
s.tool(
|
||||
'update_all_libraries',
|
||||
"Updates every library reference in a project's Library Manager to a target version (default '*' = always-newest installed). By default skips system-pinned references; set includeSystem=true to also rewrite those (risky for device-tied projects). Saves the project on success.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue