feat: add install_addon_from_file MCP tool (wraps APInstaller.CLI)
Third and final tool in the library-bring-up trio. Where install_library_file
and install_library_from_url handle plain .library files via the in-IDE
scriptengine, this one handles .package bundles (e.g. WAGO PFC libraries
package, vendor add-ons) by shelling out to the standalone CODESYS
Installer's CLI.
Command issued:
APInstaller.CLI.exe --installAddOnFromFile
--location <install-folder>
--sourcefile <package-file>
Defaults:
- APInstaller path: C:\Program Files (x86)\CODESYS\APInstaller\
APInstaller.CLI.exe (override via CODESYS_APINSTALLER_CLI env var)
- --location: derived from this MCP's configured codesysPath
(path.dirname x3) so it matches the install the MCP itself uses.
Caller can override via the 'installation' tool arg.
Why a separate tool (vs. extending install_library_file): the .package
format and .library format have different installers and different
side-effects on the CODESYS install. Keeping the tools separate keeps the
error surface and the MCP tool description honest about what each does.
Returns full stdout/stderr in the response so failed installs surface the
APInstaller's own diagnostics (dependency conflicts, permission errors)
to the caller.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7fcca42a53
commit
79c83fd4c3
1 changed files with 91 additions and 0 deletions
|
|
@ -8,6 +8,7 @@ import * as fs from 'fs';
|
|||
import * as os from 'os';
|
||||
import * as crypto from 'crypto';
|
||||
import { URL } from 'url';
|
||||
import { spawn } from 'child_process';
|
||||
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { z } from 'zod';
|
||||
|
|
@ -17,6 +18,32 @@ import { HeadlessExecutor } from './headless';
|
|||
import { ScriptManager } from './script-manager';
|
||||
import { serverLog, setLogLevel } from './logger';
|
||||
|
||||
/** Default install path for the standalone CODESYS Installer (APInstaller). */
|
||||
const DEFAULT_APINSTALLER_CLI = 'C:\\Program Files (x86)\\CODESYS\\APInstaller\\APInstaller.CLI.exe';
|
||||
|
||||
/** Locate APInstaller.CLI.exe -- env var override wins, otherwise the default path. */
|
||||
function locateAPInstallerCli(): string | null {
|
||||
const fromEnv = process.env.CODESYS_APINSTALLER_CLI;
|
||||
const candidate = fromEnv && fromEnv.trim().length > 0 ? fromEnv : DEFAULT_APINSTALLER_CLI;
|
||||
return fs.existsSync(candidate) ? candidate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a process to completion, capturing stdout+stderr. Resolves with
|
||||
* { code, stdout, stderr }. Rejects only on spawn error (e.g. exe missing).
|
||||
*/
|
||||
function runProcess(exe: string, args: string[]): Promise<{ code: number | null; stdout: string; stderr: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(exe, args, { windowsHide: true });
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.on('data', (b) => { stdout += b.toString('utf-8'); });
|
||||
child.stderr.on('data', (b) => { stderr += b.toString('utf-8'); });
|
||||
child.on('error', reject);
|
||||
child.on('close', (code) => resolve({ code, stdout, stderr }));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a URL to a temp file. Returns the local file path.
|
||||
* Follows redirects, fails on non-2xx, throws on network errors.
|
||||
|
|
@ -1089,6 +1116,70 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
|
|||
}
|
||||
);
|
||||
|
||||
s.tool(
|
||||
'install_addon_from_file',
|
||||
"Installs a CODESYS .package add-on (e.g. WAGO PFC libraries bundle, vendor packages) into a CODESYS installation by shelling out to APInstaller.CLI.exe --installAddOnFromFile. Use this for .package bundles; use install_library_file for plain .library files. Does not need CODESYS UI to be running.",
|
||||
{
|
||||
packageFilePath: z.string().describe("Full path to the .package file to install."),
|
||||
installation: z.string().optional().describe("Installation location (e.g. 'C:\\\\Program Files\\\\CODESYS 3.5.21.50'). Defaults to the parent of this MCP's configured CODESYS install."),
|
||||
},
|
||||
async (args: { packageFilePath: string; installation?: string }) => {
|
||||
const cli = locateAPInstallerCli();
|
||||
if (!cli) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `APInstaller.CLI.exe not found. Set CODESYS_APINSTALLER_CLI env var, or install the standalone CODESYS Installer (default path: ${DEFAULT_APINSTALLER_CLI}).`,
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
const pkg = resolvePath(args.packageFilePath, workspaceDir);
|
||||
if (!fs.existsSync(pkg)) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Package file not found: ${pkg}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
// Default installation location = parent of CODESYS\Common\CODESYS.exe
|
||||
// i.e. three dirnames up from config.codesysPath
|
||||
const defaultLocation = path.dirname(path.dirname(path.dirname(config.codesysPath)));
|
||||
const location = args.installation && args.installation.trim().length > 0
|
||||
? path.normalize(args.installation)
|
||||
: defaultLocation;
|
||||
|
||||
const cliArgs = ['--installAddOnFromFile', '--location', location, '--sourcefile', pkg];
|
||||
let res;
|
||||
try {
|
||||
res = await runProcess(cli, cliArgs);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Failed to spawn ${cli}: ${msg}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
const success = res.code === 0;
|
||||
const summary = success
|
||||
? `Add-on installed: ${pkg} -> ${location}`
|
||||
: `APInstaller exited with code ${res.code}.`;
|
||||
const detail = [
|
||||
summary,
|
||||
'',
|
||||
`Command: "${cli}" ${cliArgs.map((a) => (a.includes(' ') ? `"${a}"` : a)).join(' ')}`,
|
||||
'',
|
||||
'--- stdout ---',
|
||||
res.stdout || '(empty)',
|
||||
'--- stderr ---',
|
||||
res.stderr || '(empty)',
|
||||
].join('\n');
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: detail }],
|
||||
isError: !success,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
s.tool(
|
||||
'install_library_from_url',
|
||||
"Downloads a .library file from a URL (HTTPS supported, follows redirects) and installs it into the CODESYS Library Repository. Companion to install_library_file for fully-automated bring-up from internal shares, vendor download URLs, or GitHub release assets. Does not need a project to be open.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue