0
0
Fork 0
Codesys-MCP-SP21-plus/src/bin.ts
Karstein Phobic Nyvold Kvistad 9c98e61974 feat(cli): --print-config emits ready-to-paste .mcp.json for every detected install
New flags:
- --print-config: scan installs and emit a JSON block per install with
  derived server names (codesys-sp21-patch5, codesys-sp22-patch1, etc.)
- --sp <n>: filter to one SP family; collapses entry name to 'codesys'
  when exactly one install matches
- --name <name>: override the entry name (only valid with --sp narrowing
  to one)

Side effect: --detect now reuses the same detector and additionally
prints the derived profile name + suggested server entry name per
install, so even users sticking to manual config get the values
without guessing.

Refactored install discovery into src/detect.ts so both --detect and
--print-config share one implementation. New unit test fixture covers
version parsing, missing-exe, dedup, sort order, --sp filter behaviour,
--name override constraints, and verifies the emitted JSON parses back
once // comments are stripped.

The output also surfaces the multi-install caveat from launcher.ts:
the double-spawn guard refuses to start a second CODESYS.exe even on
a different exe path, so only one configured entry can be active at
a time.
2026-04-27 20:22:15 +02:00

115 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
/**
* CLI entry point for codesys-mcp-sp21-plus (Codesys-MCP-SP21+).
*/
import { program } from 'commander';
import { startMcpServer } from './server';
import { ServerConfig, ExecutionMode } from './types';
import { detectInstalls, printConfig } from './detect';
let version = '0.1.0';
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pkg = require('../package.json');
version = pkg.version;
} catch {
// ignore
}
program
.name('codesys-mcp-sp21-plus')
.description('MCP server for CODESYS with persistent UI instance')
.version(version)
.option(
'-p, --codesys-path <path>',
'Path to CODESYS executable',
process.env.CODESYS_PATH || 'C:\\Program Files\\CODESYS 3.5.21.0\\CODESYS\\Common\\CODESYS.exe'
)
.option(
'-f, --codesys-profile <profile>',
'CODESYS profile name',
process.env.CODESYS_PROFILE || 'CODESYS V3.5 SP21'
)
.option(
'-w, --workspace <dir>',
'Workspace directory for relative project paths',
process.cwd()
)
.option(
'-m, --mode <mode>',
'Execution mode: persistent (UI) or headless (--noUI)',
'persistent'
)
.option('--no-auto-launch', 'Do not auto-launch CODESYS on startup')
.option('--fallback-headless', 'Fall back to headless if persistent fails', true)
.option('--keep-alive', 'Keep CODESYS running after server stops', false)
.option('--timeout <ms>', 'Default command timeout in ms', '60000')
.option('--verbose', 'Enable verbose logging')
.option('--debug', 'Enable debug logging (more verbose)')
.option('--detect', 'Detect installed CODESYS versions and exit')
.option('--print-config', 'Print a ready-to-paste .mcp.json snippet for every detected install and exit')
.option('--sp <number>', 'With --print-config: emit only the entry for CODESYS V3.5 SP<number>')
.option('--name <name>', 'With --print-config --sp <n>: override the MCP server entry name')
.parse(process.argv);
const opts = program.opts();
if (opts.detect) {
const installs = detectInstalls();
process.stderr.write('Scanning for CODESYS installations...\n\n');
if (installs.length === 0) {
process.stderr.write(' (no installations matching "CODESYS X.Y.Z.W" found)\n');
} else {
for (const i of installs) {
process.stderr.write(` [OK] ${i.installDir}\n`);
process.stderr.write(` Exe: ${i.exePath}\n`);
process.stderr.write(` Profile: ${i.profileName}\n`);
process.stderr.write(` Suggested entry name: ${i.serverName}\n`);
}
}
process.stderr.write(`\nFound ${installs.length} CODESYS installation(s).\n`);
process.exit(0);
} else if (opts.printConfig) {
const installs = detectInstalls();
let sp: number | undefined;
if (opts.sp !== undefined) {
sp = parseInt(opts.sp, 10);
if (Number.isNaN(sp)) {
process.stderr.write(`--sp must be a number (e.g. --sp 21). Got "${opts.sp}".\n`);
process.exit(1);
}
}
try {
process.stdout.write(printConfig(installs, { sp, name: opts.name }) + '\n');
process.exit(0);
} catch (err) {
process.stderr.write(`${(err as Error).message}\n`);
process.exit(1);
}
} else {
// Build server config
const config: ServerConfig = {
codesysPath: opts.codesysPath.trim(),
profileName: opts.codesysProfile.trim(),
workspaceDir: opts.workspace.trim(),
autoLaunch: opts.autoLaunch !== false,
keepAlive: opts.keepAlive || false,
timeoutMs: parseInt(opts.timeout, 10) || 60000,
fallbackHeadless: opts.fallbackHeadless !== false,
verbose: opts.verbose || false,
debug: opts.debug || false,
mode: (opts.mode === 'headless' ? 'headless' : 'persistent') as ExecutionMode,
};
process.stderr.write(`Starting CODESYS MCP Server v${version}\n`);
process.stderr.write(` CODESYS Path: ${config.codesysPath}\n`);
process.stderr.write(` Profile: ${config.profileName}\n`);
process.stderr.write(` Mode: ${config.mode}\n`);
process.stderr.write(` Auto-launch: ${config.autoLaunch}\n`);
startMcpServer(config).catch((err) => {
process.stderr.write(`FATAL: ${err.message}\n`);
process.exit(1);
});
}