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/cli/lib/config.js CHANGED
@@ -88,9 +88,12 @@ function getGeneXusVersionCatalog() {
88
88
  const catalog = JSON.parse(fs.readFileSync(candidate, 'utf8'));
89
89
  if (!catalog || !catalog.primaryMajor || !Array.isArray(catalog.supportedMajors)) continue;
90
90
  const entries = catalog.supportedMajors.filter((entry) => entry && /^\d+$/.test(String(entry.major)));
91
+ const legacyEntries = Array.isArray(catalog.legacyMajors)
92
+ ? catalog.legacyMajors.filter((entry) => entry && /^(?:10\.[1-3]|\d+)$/.test(String(entry.major)))
93
+ : [];
91
94
  const primary = entries.find((entry) => String(entry.major) === String(catalog.primaryMajor));
92
95
  if (entries.length > 0 && primary?.defaultInstallPath) {
93
- return { ...catalog, supportedMajors: entries };
96
+ return { ...catalog, supportedMajors: entries, legacyMajors: legacyEntries };
94
97
  }
95
98
  } catch {
96
99
  }
@@ -101,13 +104,19 @@ function getGeneXusVersionCatalog() {
101
104
  { major: '17', displayName: 'GeneXus 17', defaultInstallPath: 'C:\\Program Files (x86)\\GeneXus\\GeneXus17Trial' },
102
105
  { major: '18', displayName: 'GeneXus 18', defaultInstallPath: 'C:\\Program Files (x86)\\GeneXus\\GeneXus18' }
103
106
  ],
107
+ legacyMajors: [
108
+ { major: '10.3', displayName: 'GeneXus Evolution 3', defaultInstallPath: 'C:\\Program Files (x86)\\GeneXus\\GeneXusXEv3' },
109
+ { major: '9', displayName: 'GeneXus 9.0', defaultInstallPath: 'C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 9.0' },
110
+ { major: '8', displayName: 'GeneXus 8.0', defaultInstallPath: 'C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 8.0' }
111
+ ],
104
112
  source: 'built-in-fallback'
105
113
  };
106
114
  }
107
115
 
108
116
  function getGeneXusCatalogEntries(preferredMajor = null) {
109
117
  const catalog = getGeneXusVersionCatalog();
110
- return [...catalog.supportedMajors].sort((left, right) => {
118
+ const all = [...catalog.supportedMajors, ...(catalog.legacyMajors || [])];
119
+ return all.sort((left, right) => {
111
120
  const leftPreferred = preferredMajor !== null && String(left.major) === String(preferredMajor);
112
121
  const rightPreferred = preferredMajor !== null && String(right.major) === String(preferredMajor);
113
122
  if (leftPreferred !== rightPreferred) return Number(rightPreferred) - Number(leftPreferred);
@@ -708,6 +717,12 @@ function resolveOpenCodeConfigPath(xdgConfig) {
708
717
  return path.join(xdgConfig, 'opencode', 'opencode.json');
709
718
  }
710
719
 
720
+ function alternateOpenCodeConfigPath(filePath) {
721
+ return path.extname(filePath).toLowerCase() === '.jsonc'
722
+ ? filePath.slice(0, -1)
723
+ : filePath.replace(/\.json$/i, '.jsonc');
724
+ }
725
+
711
726
  // VS Code stores its user profile (and native MCP mcp.json) in a per-platform
712
727
  // location. `variant` is 'Code' (stable) or 'Code - Insiders'.
713
728
  function vscodeUserDir(variant, { appData, macAppSupport, xdgConfig }) {
@@ -807,6 +822,7 @@ function getClientConfigTargets() {
807
822
  name: 'OpenCode (CLI)',
808
823
  format: 'opencode',
809
824
  path: resolveOpenCodeConfigPath(xdgConfig),
825
+ alternatePaths: [alternateOpenCodeConfigPath(resolveOpenCodeConfigPath(xdgConfig))],
810
826
  installMarkers: [
811
827
  path.join(xdgConfig, 'opencode'),
812
828
  path.join(home, '.local', 'share', 'opencode')
@@ -826,6 +842,7 @@ function getClientConfigTargets() {
826
842
  name: 'OpenCode Desktop',
827
843
  format: 'opencode',
828
844
  path: resolveOpenCodeConfigPath(xdgConfig),
845
+ alternatePaths: [alternateOpenCodeConfigPath(resolveOpenCodeConfigPath(xdgConfig))],
829
846
  detectByMarkerOnly: true,
830
847
  installMarkers: [
831
848
  path.join(localAppData, 'Programs', '@opencode-aidesktop'),
@@ -937,21 +954,27 @@ function clientCommandHealth(entry, client = null, { fs: fileSystem = fs } = {})
937
954
  reason: null,
938
955
  structuralState: 'indeterminate',
939
956
  semanticState: 'unknown',
940
- semanticReason: null
957
+ semanticReason: null,
958
+ pathDrift: false,
959
+ pathDriftReason: null
941
960
  };
942
961
  const invalid = (reason, structuralState = 'present') => ({
943
962
  stale: true,
944
963
  reason,
945
964
  structuralState,
946
965
  semanticState: 'invalid',
947
- semanticReason: reason
966
+ semanticReason: reason,
967
+ pathDrift: false,
968
+ pathDriftReason: null
948
969
  });
949
970
  const valid = (structuralState = 'present') => ({
950
971
  stale: false,
951
972
  reason: null,
952
973
  structuralState,
953
974
  semanticState: 'valid',
954
- semanticReason: null
975
+ semanticReason: null,
976
+ pathDrift: false,
977
+ pathDriftReason: null
955
978
  });
956
979
  const missingLauncherReason = 'configured launcher does not exist on disk';
957
980
 
@@ -961,7 +984,9 @@ function clientCommandHealth(entry, client = null, { fs: fileSystem = fs } = {})
961
984
  reason: null,
962
985
  structuralState: 'not-registered',
963
986
  semanticState: 'not-applicable',
964
- semanticReason: null
987
+ semanticReason: null,
988
+ pathDrift: false,
989
+ pathDriftReason: null
965
990
  };
966
991
  }
967
992
  if (typeof entry !== 'object') {
@@ -973,7 +998,9 @@ function clientCommandHealth(entry, client = null, { fs: fileSystem = fs } = {})
973
998
  reason: null,
974
999
  structuralState: 'not-applicable',
975
1000
  semanticState: 'not-applicable',
976
- semanticReason: null
1001
+ semanticReason: null,
1002
+ pathDrift: false,
1003
+ pathDriftReason: null
977
1004
  };
978
1005
  }
979
1006
 
@@ -1004,13 +1031,21 @@ function clientCommandHealth(entry, client = null, { fs: fileSystem = fs } = {})
1004
1031
  };
1005
1032
  }
1006
1033
  if (!exists) return invalid(missingLauncherReason, 'missing');
1034
+ // A gateway exe that exists at a different absolute path is a *different,
1035
+ // working* install — a local checkout's publish/ copy, a fixed-path install,
1036
+ // or another package cache — not a broken registration. Reporting it as
1037
+ // `stale` made `npx genexus-mcp clients` tell a checkout operator to
1038
+ // re-register, which rewrote the harness away from the checkout gateway
1039
+ // (Issue #210). The drift is still surfaced (as `pathDrift`, never `stale`)
1040
+ // and doctor's `client_config_sync` check keeps the "not this CLI's packaged
1041
+ // gateway" warning with its own remediation.
1007
1042
  if (client && client.preferDirectGateway) {
1008
1043
  const currentPackageGateway = getGatewayExePath();
1009
- if (fs.existsSync(currentPackageGateway) && normalizeExePath(command) !== normalizeExePath(currentPackageGateway)) {
1044
+ if (fileSystem.existsSync(currentPackageGateway) && normalizeExePath(command) !== normalizeExePath(currentPackageGateway)) {
1010
1045
  return {
1011
1046
  ...valid('present'),
1012
- stale: true,
1013
- reason: 'configured launcher points at a different package gateway; re-register the client'
1047
+ pathDrift: true,
1048
+ pathDriftReason: `configured gateway differs from this CLI's gateway (${currentPackageGateway})`
1014
1049
  };
1015
1050
  }
1016
1051
  }
@@ -1131,13 +1166,16 @@ function clientsStatus(opts = {}) {
1131
1166
  serverName,
1132
1167
  isThirdParty: Boolean(isThirdParty),
1133
1168
  writeSupported: client.writeSupported !== false,
1134
- configPath: client.path,
1169
+ configPath: entry && entry.configPath ? entry.configPath : client.path,
1170
+ configPaths: entry && entry.configPath ? [entry.configPath] : [client.path],
1135
1171
  command: entry && entry.command ? entry.command : null,
1136
1172
  args: entry && Array.isArray(entry.args) ? entry.args : [],
1137
1173
  url: entry && entry.url ? entry.url : null,
1138
1174
  launcherStructuralState: health.structuralState,
1139
1175
  launcherSemanticState: health.semanticState,
1140
1176
  launcherSemanticReason: health.semanticReason,
1177
+ launcherPathDrift: health.pathDrift,
1178
+ launcherPathDriftReason: health.pathDriftReason,
1141
1179
  commandStale: health.stale,
1142
1180
  commandStaleReason: health.reason || (isThirdParty ? 'configured as third-party / HTTP MCP server (e.g. official GeneXus MCP)' : null),
1143
1181
  detectedAt: det.markerHit || (det.hasConfig ? client.path : null),
@@ -1426,7 +1464,7 @@ function getOpenCodeMcpContainer(cfgObj) {
1426
1464
  };
1427
1465
  }
1428
1466
 
1429
- function applyOpenCodeJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false, globalConfig = false, fs: fileSystem = fs } = {}) {
1467
+ function applyOpenCodeJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false, globalConfig = false, alternatePaths = [], fs: fileSystem = fs } = {}) {
1430
1468
  const parsed = fileSystem.existsSync(filePath) ? readJsonFileSafe(filePath, fileSystem) : {};
1431
1469
  if (parsed === null) throw new Error('Invalid JSON');
1432
1470
  const cfgObj = parsed || {};
@@ -1470,10 +1508,20 @@ function applyOpenCodeJson(filePath, launcher, targetConfigPath, { serverName =
1470
1508
  }
1471
1509
  }
1472
1510
  writeClientJson(filePath, cfgObj, fileSystem);
1511
+ for (const alternatePath of alternatePaths || []) {
1512
+ if (!fileSystem.existsSync(alternatePath)) continue;
1513
+ applyOpenCodeJson(alternatePath, launcher, targetConfigPath, {
1514
+ serverName,
1515
+ force,
1516
+ globalConfig,
1517
+ alternatePaths: [],
1518
+ fs: fileSystem
1519
+ });
1520
+ }
1473
1521
  }
1474
1522
 
1475
- function removeOpenCodeJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME, fs: fileSystem = fs } = {}) {
1476
- const parsed = readJsonFileSafe(filePath);
1523
+ function removeOpenCodeJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME, alternatePaths = [], fs: fileSystem = fs } = {}) {
1524
+ const parsed = readJsonFileSafe(filePath, fileSystem);
1477
1525
  if (parsed === null) throw new Error('Invalid JSON');
1478
1526
  const cfgObj = parsed || {};
1479
1527
  if (!cfgObj.mcp || typeof cfgObj.mcp !== 'object') return false;
@@ -1503,9 +1551,17 @@ function removeOpenCodeJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME, fs
1503
1551
  }
1504
1552
  }
1505
1553
  }
1506
- if (!removedAny) return false;
1507
- writeClientJson(filePath, cfgObj, fileSystem);
1508
- return true;
1554
+ if (removedAny) writeClientJson(filePath, cfgObj, fileSystem);
1555
+ let removedAlternate = false;
1556
+ for (const alternatePath of alternatePaths || []) {
1557
+ if (!fileSystem.existsSync(alternatePath)) continue;
1558
+ removedAlternate = removeOpenCodeJson(alternatePath, {
1559
+ serverName,
1560
+ alternatePaths: [],
1561
+ fs: fileSystem
1562
+ }) || removedAlternate;
1563
+ }
1564
+ return removedAny || removedAlternate;
1509
1565
  }
1510
1566
 
1511
1567
  function extractCodexTomlEntry(content, serverName = DEFAULT_MCP_SERVER_NAME) {
@@ -1650,8 +1706,49 @@ function normalizeExePath(p) {
1650
1706
  return s;
1651
1707
  }
1652
1708
 
1709
+ function readOpenCodeCommandEntry(filePath, serverName, fileSystem) {
1710
+ if (!fileSystem.existsSync(filePath)) return null;
1711
+ const parsed = readJsonFileSafe(filePath, fileSystem);
1712
+ if (!parsed || typeof parsed !== 'object') return null;
1713
+ let entry = parsed.mcp?.servers?.[serverName] || parsed.mcp?.[serverName];
1714
+ if (!entry && serverName === DEFAULT_MCP_SERVER_NAME) {
1715
+ const legacyGx = parsed.mcp?.servers?.genexus || parsed.mcp?.genexus;
1716
+ if (legacyGx && !isThirdPartyMcpEntry(legacyGx)) entry = legacyGx;
1717
+ else entry = parsed.mcp?.servers?.genexus18 || parsed.mcp?.genexus18;
1718
+ }
1719
+ if (!entry) return null;
1720
+ if (Array.isArray(entry.command) && entry.command.length > 0) {
1721
+ return {
1722
+ command: entry.command[0],
1723
+ args: entry.command.slice(1),
1724
+ url: entry.url || null,
1725
+ type: entry.type || null,
1726
+ environment: entry.environment || null,
1727
+ raw: entry,
1728
+ configPath: filePath
1729
+ };
1730
+ }
1731
+ return {
1732
+ command: null,
1733
+ args: [],
1734
+ url: entry.url || null,
1735
+ type: entry.type || null,
1736
+ environment: entry.environment || null,
1737
+ raw: entry,
1738
+ configPath: filePath
1739
+ };
1740
+ }
1741
+
1653
1742
  function readClientCommandEntry(client, serverName = DEFAULT_MCP_SERVER_NAME, { fs: fileSystem = fs } = {}) {
1654
1743
  if (client.writeSupported === false) return null;
1744
+ if (client.format === 'opencode') {
1745
+ const paths = [client.path, ...(client.alternatePaths || [])];
1746
+ for (const configPath of paths) {
1747
+ const entry = readOpenCodeCommandEntry(configPath, serverName, fileSystem);
1748
+ if (entry) return entry;
1749
+ }
1750
+ return null;
1751
+ }
1655
1752
  if (!fileSystem.existsSync(client.path)) return null;
1656
1753
  try {
1657
1754
  if (client.format === 'mcpServers') {
@@ -1752,7 +1849,7 @@ function getLocalAppDataCacheDir() {
1752
1849
  }
1753
1850
 
1754
1851
  function getGeneXusMajor(version) {
1755
- const match = String(version || '').match(/^\s*(\d+)/);
1852
+ const match = String(version || '').match(/^\s*(10\.[1-3](?!\d)|\d+)/);
1756
1853
  return match ? match[1] : null;
1757
1854
  }
1758
1855
 
@@ -1809,7 +1906,10 @@ function readGeneXusInstallationIdentity(gxPath, options = {}) {
1809
1906
  const readExecutableVersion = typeof options.readExecutableVersion === 'function'
1810
1907
  ? options.readExecutableVersion
1811
1908
  : readExecutableProductVersion;
1812
- const executableVersion = readExecutableVersion(path.join(gxPath, 'GeneXus.exe'));
1909
+ let executableVersion = readExecutableVersion(path.join(gxPath, 'GeneXus.exe'));
1910
+ if (!executableVersion) {
1911
+ executableVersion = readExecutableVersion(path.join(gxPath, 'gx.exe'));
1912
+ }
1813
1913
  if (executableVersion) {
1814
1914
  return {
1815
1915
  version: executableVersion,
@@ -1895,14 +1995,24 @@ function readGeneXusKbIdentity(kbPath) {
1895
1995
  if (!kbPath) return { version: null, major: null, source: 'unavailable', reason: 'missing-kb-path' };
1896
1996
 
1897
1997
  let gxwFiles;
1998
+ let gxiFiles = [];
1898
1999
  try {
1899
- gxwFiles = fs.readdirSync(kbPath)
2000
+ const allFiles = fs.readdirSync(kbPath);
2001
+ gxwFiles = allFiles
1900
2002
  .filter((fileName) => fileName.toLowerCase().endsWith('.gxw'))
1901
2003
  .map((fileName) => path.join(kbPath, fileName));
2004
+ gxiFiles = allFiles
2005
+ .filter((fileName) => fileName.toLowerCase().endsWith('.gxi'))
2006
+ .map((fileName) => path.join(kbPath, fileName));
1902
2007
  } catch {
1903
2008
  return { version: null, major: null, source: 'unavailable', reason: 'unreadable-kb-path' };
1904
2009
  }
1905
- if (gxwFiles.length === 0) return { version: null, major: null, source: 'unavailable', reason: 'no-gxw' };
2010
+ if (gxwFiles.length === 0) {
2011
+ if (gxiFiles.length > 0) {
2012
+ return { version: '9.0', major: '9', source: 'gxi-classic', reason: null };
2013
+ }
2014
+ return { version: null, major: null, source: 'unavailable', reason: 'no-gxw' };
2015
+ }
1906
2016
  if (gxwFiles.length > 1) return { version: null, major: null, source: 'unavailable', reason: 'multiple-gxw' };
1907
2017
 
1908
2018
  try {
package/cli/run.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const { main, EXIT_CODES } = require('./index');
2
+ const { main, EXIT_CODES, isKnownCommandToken } = require('./index');
3
3
  const { writeStructured } = require('./lib/output');
4
4
  const { operationalErrorEnvelope } = require('./commands/axi');
5
5
  const { writeLastStdioError } = require('./lib/stdio-diagnostics');
@@ -8,8 +8,9 @@ const { writeLastStdioError } = require('./lib/stdio-diagnostics');
8
8
  // as the MCP JSON-RPC channel. Writing a JSON envelope to stdout there would
9
9
  // corrupt the protocol stream. Route errors to stderr instead.
10
10
  const argv = process.argv.slice(2);
11
- const KNOWN_COMMANDS = new Set(['status', 'doctor', 'tools', 'config', 'init', 'setup', 'whoami', 'uninstall', 'kb', 'clients', 'help', 'home', 'axi', 'llm', 'layout', 'update']);
12
- const isInteractiveCommand = argv.length > 0 && KNOWN_COMMANDS.has(argv[0]);
11
+ // Issue #207: share the command table with the parser instead of keeping a second
12
+ // hand-synced copy here.
13
+ const isInteractiveCommand = argv.length > 0 && isKnownCommandToken(argv[0]);
13
14
  const errorOut = isInteractiveCommand ? process.stdout : process.stderr;
14
15
 
15
16
  main(argv)