genexus-mcp 3.5.2 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,7 @@ needed. Existing tool names, arguments, and MCP client configuration formats do
57
57
  not change.
58
58
 
59
59
  <!-- BEGIN GENERATED: gx-compatibility -->
60
- Supported SDK majors: **GeneXus 17, GeneXus 18**.
60
+ Supported SDK majors: **GeneXus 16, GeneXus 17, GeneXus 18**.
61
61
  Primary SDK: **GeneXus 18**.
62
62
  Source of truth: `config/gx-versions.json`.
63
63
  <!-- END GENERATED: gx-compatibility -->
@@ -67,6 +67,13 @@ catalog only after compiling the Worker with that SDK and passing the focused
67
67
  tests plus a live KB smoke. This prevents the server from claiming compatibility
68
68
  based only on a version string.
69
69
 
70
+ ### Legacy GeneXus compatibility (GX8 to GX15)
71
+
72
+ The server also includes best-effort dynamic compatibility for legacy installations:
73
+ - **GeneXus Evolution 1 (10.1), Evolution 2 (10.2), Evolution 3 (10.3), and GeneXus 15**: Driven via runtime reflection (`dotnet-reflection`), dynamically adapting to missing types or structural differences (such as module-less KBs without `QualifiedName`).
74
+ - **GeneXus 8.0 and GeneXus 9.0**: Driven via classic Win32 COM automation (`com-gxpublic`), late-binding to `GXPublic.GXPublic` on an STA thread to open, inspect, and read objects from classic `.gxi` Knowledge Bases.
75
+ - **Graceful degradation**: Modern tools that require features introduced in newer GeneXus versions (such as `genexus_api`, `genexus_gam`, or `genexus_module`) return structured `UNSUPPORTED_IN_GENEXUS_VERSION` errors indicating the required minimum version rather than failing ungracefully.
76
+
70
77
  ---
71
78
 
72
79
  ## What you can do with it
@@ -36,6 +36,7 @@ const {
36
36
  DEFAULT_MCP_SERVER_NAME
37
37
  } = require('../lib/config');
38
38
  const { getStdioErrorLogPath } = require('../lib/stdio-diagnostics');
39
+ const { getPackageVersion } = require('../lib/update-check');
39
40
 
40
41
  function resolveClientIds(options) {
41
42
  if (!options || !options.clients) return null;
@@ -58,7 +59,9 @@ function validateClientIds(ids) {
58
59
 
59
60
  function isSupportedCatalogMajor(catalog, major) {
60
61
  if (!major) return true;
61
- return catalog.supportedMajors.some((entry) => String(entry.major) === String(major));
62
+ const inSupported = catalog.supportedMajors && catalog.supportedMajors.some((entry) => String(entry.major) === String(major));
63
+ if (inSupported) return true;
64
+ return Array.isArray(catalog.legacyMajors) && catalog.legacyMajors.some((entry) => String(entry.major) === String(major));
62
65
  }
63
66
 
64
67
  function parseFieldSelection(raw) {
@@ -175,6 +178,37 @@ function buildStatusData(cwd) {
175
178
  return { ready, configFound, gatewayExeFound, kbLooksValid, configPath, gatewayExePath, kbPath, gxPath, configSource };
176
179
  }
177
180
 
181
+ const PROBE_EXIT_GRACE_MS = 2000;
182
+
183
+ // Stop a probe child and wait for it to actually exit. On Windows child.kill()
184
+ // only signals the direct process and returns before the OS releases the
185
+ // executable image, so reporting success right after kill() left the exe file
186
+ // handle open (Issue #211: the shared gateway stub in cli/run.test.js made the
187
+ // suite teardown fail with EPERM while every assertion passed). Resolves true
188
+ // once the child exited, false when it did not within the bounded grace.
189
+ function stopProbeChild(child, graceMs = PROBE_EXIT_GRACE_MS) {
190
+ return new Promise((resolve) => {
191
+ if (!child || child.exitCode !== null || child.signalCode !== null) {
192
+ resolve(true);
193
+ return;
194
+ }
195
+ let settled = false;
196
+ const finish = (exited) => {
197
+ if (settled) return;
198
+ settled = true;
199
+ clearTimeout(timer);
200
+ resolve(exited);
201
+ };
202
+ const timer = setTimeout(() => finish(false), graceMs);
203
+ child.once('exit', () => finish(true));
204
+ try {
205
+ child.kill();
206
+ } catch {
207
+ finish(true);
208
+ }
209
+ });
210
+ }
211
+
178
212
  async function spawnGatewayProbe({ env = process.env, spawnHoldMs, timeoutMs, label, successDetail }) {
179
213
  const gatewayExePath = getGatewayExePath();
180
214
 
@@ -208,17 +242,25 @@ async function spawnGatewayProbe({ env = process.env, spawnHoldMs, timeoutMs, la
208
242
  });
209
243
 
210
244
  child.once('spawn', () => {
211
- setTimeout(() => {
212
- try { child.kill(); } catch { }
213
- finish({ status: 'pass', detail: successDetail });
245
+ setTimeout(async () => {
246
+ const exited = await stopProbeChild(child);
247
+ if (exited) {
248
+ finish({ status: 'pass', detail: successDetail });
249
+ } else {
250
+ finish({ status: 'warn', detail: `${label}: process did not exit after the stop signal; it may still hold the gateway exe.` });
251
+ }
214
252
  }, spawnHoldMs);
215
253
  });
216
254
 
217
- setTimeout(() => {
218
- if (!done) {
219
- try { child.kill(); } catch { }
220
- finish({ status: 'warn', detail: `${label} timed out; process was force-stopped.` });
221
- }
255
+ setTimeout(async () => {
256
+ if (done) return;
257
+ const exited = await stopProbeChild(child);
258
+ finish({
259
+ status: 'warn',
260
+ detail: exited
261
+ ? `${label} timed out; process was force-stopped.`
262
+ : `${label} timed out and did not exit after the stop signal; it may still hold the gateway exe.`
263
+ });
222
264
  }, timeoutMs);
223
265
  } catch (err) {
224
266
  finish({ status: 'fail', detail: `${label} threw: ${err.message}` });
@@ -561,6 +603,50 @@ function buildInProcessBuildAssemblyLoadCheck(gxPath) {
561
603
  };
562
604
  }
563
605
 
606
+ function buildGxPublicComCheck(gxMajor, gxPath) {
607
+ if (gxMajor !== '8' && gxMajor !== '9') {
608
+ return { id: 'gxpublic_com_registration', status: 'not_applicable', detail: 'Not a GeneXus 8.0/9.0 installation.' };
609
+ }
610
+ const child_process = require('child_process');
611
+ let isRegistered = false;
612
+ try {
613
+ const res = child_process.spawnSync('reg', ['query', 'HKCR\\GXPublic.GXPublic'], { encoding: 'utf8', timeout: 2000 });
614
+ isRegistered = res.status === 0;
615
+ } catch { }
616
+
617
+ if (isRegistered) {
618
+ return { id: 'gxpublic_com_registration', status: 'pass', detail: 'GXPublic COM automation server (GXPublic.GXPublic) is registered.' };
619
+ }
620
+ const dllHint = gxPath ? path.join(gxPath, 'GxPublic.dll') : 'GxPublic.dll';
621
+ return {
622
+ id: 'gxpublic_com_registration',
623
+ status: 'warn',
624
+ detail: `GXPublic COM component is not registered. Run 'regsvr32 "${dllHint}"' to enable GeneXus ${gxMajor}.0 COM automation.`
625
+ };
626
+ }
627
+
628
+ function buildLegacyIdeLockCheck(gxMajor) {
629
+ if (gxMajor !== '8' && gxMajor !== '9' && gxMajor !== '10.1' && gxMajor !== '10.2') {
630
+ return { id: 'legacy_ide_lock', status: 'not_applicable', detail: 'Lock check not applicable for modern GeneXus versions.' };
631
+ }
632
+ const child_process = require('child_process');
633
+ let ideRunning = false;
634
+ try {
635
+ const res = child_process.spawnSync('tasklist', ['/FI', 'IMAGENAME eq gx.exe', '/NH'], { encoding: 'utf8', timeout: 2000 });
636
+ if (res.stdout && res.stdout.toLowerCase().includes('gx.exe')) {
637
+ ideRunning = true;
638
+ }
639
+ } catch { }
640
+ if (ideRunning) {
641
+ return {
642
+ id: 'legacy_ide_lock',
643
+ status: 'warn',
644
+ detail: 'GeneXus IDE (gx.exe) process is currently running. Legacy engines hold exclusive file locks on the KB.'
645
+ };
646
+ }
647
+ return { id: 'legacy_ide_lock', status: 'pass', detail: 'No conflicting GeneXus IDE process (gx.exe) detected.' };
648
+ }
649
+
564
650
  function redactConfig(cfg) {
565
651
  // Replace absolute paths with `<redacted:hash8>` so the structure is preserved
566
652
  // but filesystem layout, usernames, and KB names are not leaked. Hash is stable
@@ -709,7 +795,7 @@ async function handleDoctor(options, ctx) {
709
795
  const kbPath = data.kbPath;
710
796
  const gxPath = data.gxPath;
711
797
  const kbExists = !!(kbPath && fs.existsSync(kbPath));
712
- const gxExeExists = !!(gxPath && fs.existsSync(path.join(gxPath, 'genexus.exe')));
798
+ const gxExeExists = !!(gxPath && (fs.existsSync(path.join(gxPath, 'genexus.exe')) || fs.existsSync(path.join(gxPath, 'gx.exe'))));
713
799
  const kbSdkCompatibility = kbExists && gxExeExists
714
800
  ? compareGeneXusKbAndInstallation(kbPath, gxPath)
715
801
  : null;
@@ -730,15 +816,18 @@ async function handleDoctor(options, ctx) {
730
816
  compatibilityDetail = `${unsupportedCompatibilityMajor} is not supported by this MCP distribution. Supported majors: ${catalog.supportedMajors.map((entry) => entry.major).join(', ')}.`;
731
817
  } else if (kbSdkCompatibility.status === 'mismatch') {
732
818
  compatibilityStatus = 'fail';
733
- compatibilityDetail = `KB major ${kbSdkCompatibility.kb.major} does not match GeneXus SDK major ${kbSdkCompatibility.gx.major}. Re-run init with the matching --gx path.`;
819
+ compatibilityDetail = `KB major ${kbSdkCompatibility.kb.major} does not match GeneXus SDK major ${kbSdkCompatibility.gx.major}${kbSdkCompatibility.gx.version ? ` (${kbSdkCompatibility.gx.version})` : ''}. Re-run init with the matching --gx path.`;
734
820
  } else if (kbSdkCompatibility.status === 'match') {
735
821
  compatibilityStatus = 'pass';
736
- compatibilityDetail = `KB major ${kbSdkCompatibility.kb.major} matches GeneXus SDK major ${kbSdkCompatibility.gx.major}.`;
822
+ compatibilityDetail = `KB major ${kbSdkCompatibility.kb.major} matches GeneXus SDK major ${kbSdkCompatibility.gx.major}${kbSdkCompatibility.gx.version ? ` (${kbSdkCompatibility.gx.version})` : ''}.`;
737
823
  } else {
738
824
  compatibilityDetail = `KB/SDK major compatibility could not be verified (KB: ${kbSdkCompatibility.kb.source}; SDK: ${kbSdkCompatibility.gx.source}).`;
739
825
  }
740
826
  }
741
827
 
828
+ const gxIdent = gxPath ? readGeneXusInstallationIdentity(gxPath) : null;
829
+ const gxVersionLabel = gxIdent && gxIdent.version ? ` (GeneXus ${gxIdent.major || ''} version ${gxIdent.version})` : (gxIdent && gxIdent.major ? ` (GeneXus ${gxIdent.major})` : '');
830
+
742
831
  const riskyZone = isPathLikelyAppLockerBlocked(gatewayExePath);
743
832
  const clientCrossCheck = buildClientExeCrossCheck(gatewayExePath);
744
833
 
@@ -766,7 +855,7 @@ async function handleDoctor(options, ctx) {
766
855
  // Same logic for the GeneXus install: missing genexus.exe at a configured path
767
856
  // guarantees a worker crash on first MCP call. Promote from warn to fail so init
768
857
  // exits non-zero and the caller (install.ps1, AI client) actually sees the problem.
769
- { id: 'gx_installation', status: gxExeExists ? 'pass' : (gxPath ? 'fail' : 'warn'), detail: gxExeExists ? 'GeneXus installation has genexus.exe.' : (gxPath ? `Configured GeneXus installation is missing genexus.exe at: ${gxPath}` : 'No GeneXus installation path is configured.') },
858
+ { id: 'gx_installation', status: gxExeExists ? 'pass' : (gxPath ? 'fail' : 'warn'), detail: gxExeExists ? `GeneXus installation has ${gxPath && fs.existsSync(path.join(gxPath, 'gx.exe')) && !fs.existsSync(path.join(gxPath, 'genexus.exe')) ? 'gx.exe' : 'genexus.exe'}${gxVersionLabel}.` : (gxPath ? `Configured GeneXus installation is missing executable at: ${gxPath}` : 'No GeneXus installation path is configured.') },
770
859
  {
771
860
  id: 'kb_sdk_compatibility',
772
861
  status: compatibilityStatus,
@@ -783,6 +872,13 @@ async function handleDoctor(options, ctx) {
783
872
  const inProcessLoad = buildInProcessBuildAssemblyLoadCheck(gxPath);
784
873
  checks.push({ id: 'in_process_build_assembly_load', status: inProcessLoad.status, detail: inProcessLoad.detail });
785
874
 
875
+ // Legacy GeneXus checks (GX 8.0, 9.0, Ev1, Ev2)
876
+ const gxMajor = (gxIdent && gxIdent.major) || (kbSdkCompatibility && kbSdkCompatibility.gx && kbSdkCompatibility.gx.major) || null;
877
+ const comCheck = buildGxPublicComCheck(gxMajor, gxPath);
878
+ checks.push({ id: 'gxpublic_com_registration', status: comCheck.status, detail: comCheck.detail });
879
+ const ideLockCheck = buildLegacyIdeLockCheck(gxMajor);
880
+ checks.push({ id: 'legacy_ide_lock', status: ideLockCheck.status, detail: ideLockCheck.detail });
881
+
786
882
  // Client registration summary — one line answering "are my AI agents wired up?".
787
883
  const clientRows = clientsStatus();
788
884
  const installedRows = clientRows.filter((r) => r.installed);
@@ -1343,6 +1439,7 @@ function buildClientLauncherHelp(patchResult) {
1343
1439
  }
1344
1440
  if (patched.length > 0 && process.platform === 'win32' && !process.env.GENEXUS_MCP_GATEWAY_EXE) {
1345
1441
  help.push('Windows launcher paths may resolve under the npm cache and be blocked by AppLocker/SRP. Use scripts/install.ps1 for a stable whitelisted path.');
1442
+ help.push('Working from a local checkout? Register the clients with the checkout gateway from the repo root instead: `$env:GENEXUS_MCP_GATEWAY_EXE="<repoRoot>\\publish\\GxMcp.Gateway.exe"; node cli\\run.js clients add --clients <ids>` (or re-run `.\\install.ps1`). Avoid `npx @latest clients add` there, because it rewrites the registration to this npm package\'s launcher; validate with the same checkout CLI (`node cli\\run.js clients` / `doctor`).');
1346
1443
  }
1347
1444
  return help;
1348
1445
  }
@@ -1938,6 +2035,7 @@ async function probeWorkerStartup({ configPath, observeMs = 2500 }) {
1938
2035
  };
1939
2036
 
1940
2037
  let child;
2038
+ let stopping = false;
1941
2039
  try {
1942
2040
  child = spawn(gatewayExePath, [], {
1943
2041
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -1956,6 +2054,9 @@ async function probeWorkerStartup({ configPath, observeMs = 2500 }) {
1956
2054
  });
1957
2055
 
1958
2056
  child.once('exit', (code) => {
2057
+ // The observation timeout stops the gateway itself; that deliberate
2058
+ // signal is reported by the timeout branch, not as a crash.
2059
+ if (stopping) return;
1959
2060
  if (code === 0) {
1960
2061
  finish({ status: 'pass', detail: 'Worker smoke: gateway exited cleanly during observation window.' });
1961
2062
  } else {
@@ -1969,10 +2070,15 @@ async function probeWorkerStartup({ configPath, observeMs = 2500 }) {
1969
2070
  }
1970
2071
  });
1971
2072
 
1972
- setTimeout(() => {
1973
- try { child.kill(); } catch { }
1974
- // Still alive after observeMs → worker bootstrapped without crashing.
1975
- finish({ status: 'pass', detail: `Worker smoke: gateway stayed alive for ${observeMs}ms with KB and GX configured.` });
2073
+ setTimeout(async () => {
2074
+ stopping = true;
2075
+ const exited = await stopProbeChild(child);
2076
+ if (exited) {
2077
+ // Still alive after observeMs → worker bootstrapped without crashing.
2078
+ finish({ status: 'pass', detail: `Worker smoke: gateway stayed alive for ${observeMs}ms with KB and GX configured.` });
2079
+ } else {
2080
+ finish({ status: 'warn', detail: 'Worker smoke: gateway did not exit after the stop signal; it may still hold the gateway exe.' });
2081
+ }
1976
2082
  }, observeMs);
1977
2083
  });
1978
2084
  }
@@ -2149,11 +2255,12 @@ async function handleClients(subcommand, options, ctx) {
2149
2255
  if (invalidLaunchers.length > 0) {
2150
2256
  help.push(`These clients have a known-invalid MCP launcher (check command and args) — re-register: genexus-mcp clients add --clients ${invalidLaunchers.map((r) => r.id).join(',')}${serverName !== DEFAULT_MCP_SERVER_NAME ? ` --server-name ${serverName}` : ''}`);
2151
2257
  }
2152
- const otherStale = stale.filter((r) =>
2153
- r.launcherStructuralState !== 'missing' && r.launcherSemanticState !== 'invalid'
2154
- );
2155
- if (otherStale.length > 0) {
2156
- help.push(`These clients have a stale launcher — re-register: genexus-mcp clients add --clients ${otherStale.map((r) => r.id).join(',')}${serverName !== DEFAULT_MCP_SERVER_NAME ? ` --server-name ${serverName}` : ''}`);
2258
+ // A launcher that exists but is not this CLI's gateway is a different working
2259
+ // install (checkout, fixed-path, other package cache), so it is reported as
2260
+ // informational drift instead of a stale registration that needs repair.
2261
+ const driftedLaunchers = rows.filter((r) => r.launcherPathDrift && !r.commandStale);
2262
+ if (driftedLaunchers.length > 0) {
2263
+ help.push(`Note: ${driftedLaunchers.map((r) => r.name).join(', ')} point at a gateway that is not this CLI's (informational — the launcher exists and is valid). Re-register with this CLI only if you want them to track it: genexus-mcp clients add --clients ${driftedLaunchers.map((r) => r.id).join(',')}${serverName !== DEFAULT_MCP_SERVER_NAME ? ` --server-name ${serverName}` : ''}`);
2157
2264
  }
2158
2265
  const semanticValidCount = rows.filter((r) => r.launcherSemanticState === 'valid').length;
2159
2266
  const semanticInvalidCount = rows.filter((r) => r.launcherSemanticState === 'invalid').length;
@@ -2483,6 +2590,13 @@ function commandHelpMap() {
2483
2590
  'genexus-mcp update --channel next # check the @next dist-tag'
2484
2591
  ]
2485
2592
  },
2593
+ version: {
2594
+ usage: 'genexus-mcp --version | -v | version [--format toon|json|text]',
2595
+ examples: [
2596
+ 'genexus-mcp --version # prints the bare version (scripts/CI)',
2597
+ 'genexus-mcp -v --format json # axi-cli/1 envelope with ok.version'
2598
+ ]
2599
+ },
2486
2600
  layout: {
2487
2601
  usage: 'genexus-mcp layout status [--title "GeneXus"] [--format ...] OR genexus-mcp layout run --action <focus|activate-layout|activate-tab|send-keys|type-text|click> [--tab "Layout"] [--keys "..."] [--text "..."] [--x N --y N] [--title "..."] [--format ...] OR genexus-mcp layout inspect [--tab "Layout"] [--limit N] [--full] [--title "..."] [--format ...]',
2488
2602
  examples: ['genexus-mcp layout status --format json', 'genexus-mcp layout run --action activate-tab --tab "Layout" --format json', 'genexus-mcp layout inspect --tab Layout --format json']
@@ -2518,6 +2632,34 @@ async function handleHome(_options, ctx) {
2518
2632
  };
2519
2633
  }
2520
2634
 
2635
+ // Issue #207: `genexus-mcp --version` / `-v` / `version` used to fall through to the
2636
+ // gateway passthrough (which never answers that token) and exit 0 with no output — a
2637
+ // silent false positive for install checks. The package version is read from the same
2638
+ // helper the update flow uses.
2639
+ // `deps` is an injection seam so the unreadable-package.json path is testable.
2640
+ async function handleVersion(_options, ctx, deps = {}) {
2641
+ const readVersion = deps.getPackageVersion || getPackageVersion;
2642
+ const version = readVersion();
2643
+ if (!version) {
2644
+ return {
2645
+ exitCode: ctx.EXIT_CODES.ERROR,
2646
+ envelope: operationalErrorEnvelope(
2647
+ 'Could not read the genexus-mcp package version (package.json missing or unreadable).',
2648
+ ctx.EXIT_CODES.ERROR,
2649
+ ['Reinstall the CLI (`genexus-mcp update`) so package.json can be read.'],
2650
+ 'version_unavailable'
2651
+ )
2652
+ };
2653
+ }
2654
+ return {
2655
+ exitCode: ctx.EXIT_CODES.OK,
2656
+ envelope: {
2657
+ ok: { version },
2658
+ help: []
2659
+ }
2660
+ };
2661
+ }
2662
+
2521
2663
  async function handleHelp(targetCommand, ctx) {
2522
2664
  const binPath = collapseHome(process.argv[1] || process.execPath);
2523
2665
  const map = commandHelpMap();
@@ -2547,7 +2689,7 @@ async function handleHelp(targetCommand, ctx) {
2547
2689
  bin: binPath,
2548
2690
  command: 'genexus-mcp',
2549
2691
  description: 'GeneXus MCP launcher and AXI-oriented utility CLI',
2550
- commands: ['home', 'axi home', 'status', 'doctor', 'tools list', 'config show', 'config create', 'layout status', 'layout run', 'layout inspect', 'init', 'whoami', 'uninstall', 'kb list', 'kb add', 'kb remove', 'kb switch', 'llm help', 'update', 'help'],
2692
+ commands: ['home', 'axi home', 'status', 'doctor', 'tools list', 'config show', 'config create', 'layout status', 'layout run', 'layout inspect', 'init', 'whoami', 'uninstall', 'kb list', 'kb add', 'kb remove', 'kb switch', 'llm help', 'update', 'version', 'help'],
2551
2693
  defaults: { format: 'toon', limit: 100 }
2552
2694
  },
2553
2695
  help: [
@@ -2660,6 +2802,7 @@ module.exports = {
2660
2802
  handleLlmHelp,
2661
2803
  handleLayout,
2662
2804
  handleHelp,
2805
+ handleVersion,
2663
2806
  usageEnvelope,
2664
2807
  operationalErrorEnvelope,
2665
2808
  resolveMcpSmokeTarget,
package/cli/index.js CHANGED
@@ -26,6 +26,7 @@ const {
26
26
  handleLlmHelp,
27
27
  handleLayout,
28
28
  handleHelp,
29
+ handleVersion,
29
30
  usageEnvelope,
30
31
  commandHelpMap
31
32
  } = require('./commands/axi');
@@ -63,7 +64,19 @@ const GLOBAL_DEFAULTS = {
63
64
  help: false
64
65
  };
65
66
 
66
- const KNOWN_COMMANDS = new Set(['status', 'doctor', 'tools', 'config', 'init', 'setup', 'whoami', 'uninstall', 'kb', 'clients', 'help', 'home', 'axi', 'llm', 'layout', 'update']);
67
+ // Single source of truth for command routing: cli/run.js imports both sets so the
68
+ // AXI-vs-passthrough decision (stdout vs stderr for unhandled errors) cannot drift
69
+ // from the parser again — issue #207 was caused by two hand-synced copies.
70
+ const KNOWN_COMMANDS = new Set(['status', 'doctor', 'tools', 'config', 'init', 'setup', 'whoami', 'uninstall', 'kb', 'clients', 'help', 'home', 'axi', 'llm', 'layout', 'update', 'version']);
71
+
72
+ // Version query aliases. `-v` deliberately is NOT an alias of `--help`/`-h`: those
73
+ // return immediately, while the version aliases are command tokens so that remaining
74
+ // flags (`-v --format json`) are still parsed and honored (issue #207).
75
+ const VERSION_ALIASES = new Set(['version', '-v', '--version']);
76
+
77
+ function isKnownCommandToken(token) {
78
+ return KNOWN_COMMANDS.has(token) || VERSION_ALIASES.has(token);
79
+ }
67
80
 
68
81
  function parseArgs(argv) {
69
82
  const result = {
@@ -79,7 +92,8 @@ function parseArgs(argv) {
79
92
  if (tokens.length === 0) return result;
80
93
 
81
94
  const first = tokens[0];
82
- if (!KNOWN_COMMANDS.has(first) && !first.startsWith('--')) {
95
+ const versionIntent = VERSION_ALIASES.has(first);
96
+ if (!versionIntent && !KNOWN_COMMANDS.has(first) && !first.startsWith('--')) {
83
97
  return result;
84
98
  }
85
99
 
@@ -89,7 +103,13 @@ function parseArgs(argv) {
89
103
  return result;
90
104
  }
91
105
 
92
- if (KNOWN_COMMANDS.has(first)) {
106
+ if (versionIntent) {
107
+ // Treat the alias as a consumed command token and keep parsing the remaining
108
+ // flags, so `version --format json`, `-v --format json` and `--version --format json`
109
+ // all reach the format validation instead of falling through to passthrough.
110
+ result.command = 'version';
111
+ tokens.shift();
112
+ } else if (KNOWN_COMMANDS.has(first)) {
93
113
  result.command = first === 'setup' ? 'init' : first;
94
114
  tokens.shift();
95
115
  }
@@ -534,7 +554,9 @@ function resolveMetaCommand(parsed, targetHelp) {
534
554
  async function main(argv) {
535
555
  const parsed = parseArgs(argv);
536
556
 
537
- if (parsed.command !== 'update') {
557
+ // `version` is a quiet query: no update-check banner (it would corrupt the raw
558
+ // version string scripts read) and no launcher config side effects.
559
+ if (parsed.command !== 'update' && parsed.command !== 'version') {
538
560
  startBackgroundUpdateCheck({ quiet: parsed.options.quiet });
539
561
  }
540
562
 
@@ -568,6 +590,19 @@ async function main(argv) {
568
590
  return helpResult.exitCode;
569
591
  }
570
592
 
593
+ if (parsed.command === 'version') {
594
+ const versionResult = await handleVersion(parsed.options, ctx);
595
+ // Default formats print the bare version so `genexus-mcp --version` is usable
596
+ // from scripts/CI; only --format json opts into the axi-cli/1 envelope.
597
+ if (versionResult.exitCode === EXIT_CODES.OK
598
+ && (parsed.options.format === 'toon' || parsed.options.format === 'text')) {
599
+ process.stdout.write(`${versionResult.envelope.ok.version}\n`);
600
+ } else {
601
+ writeStructured(process.stdout, withCommandMeta(versionResult.envelope, 'version'), parsed.options.format);
602
+ }
603
+ return versionResult.exitCode;
604
+ }
605
+
571
606
  let result;
572
607
 
573
608
  switch (parsed.command) {
@@ -682,6 +717,9 @@ module.exports = {
682
717
  main,
683
718
  parseArgs,
684
719
  EXIT_CODES,
720
+ KNOWN_COMMANDS,
721
+ VERSION_ALIASES,
722
+ isKnownCommandToken,
685
723
  renderOutput,
686
724
  formatToonObject
687
725
  };
@@ -78,10 +78,16 @@ class OpenCodeJsoncAdapter extends BaseClientAdapter {
78
78
  this.config = configModule || require('./config');
79
79
  }
80
80
  apply(client, launcher, targetConfigPath, opts = {}) {
81
- return this.config.applyOpenCodeJson(client.path, launcher, targetConfigPath, opts);
81
+ return this.config.applyOpenCodeJson(client.path, launcher, targetConfigPath, {
82
+ ...opts,
83
+ alternatePaths: client.alternatePaths || []
84
+ });
82
85
  }
83
86
  remove(client, opts = {}) {
84
- return this.config.removeOpenCodeJson(client.path, opts);
87
+ return this.config.removeOpenCodeJson(client.path, {
88
+ ...opts,
89
+ alternatePaths: client.alternatePaths || []
90
+ });
85
91
  }
86
92
  read(client, serverName) {
87
93
  if (!fs.existsSync(client.path)) return null;