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 +8 -1
- package/cli/commands/axi.js +166 -23
- package/cli/index.js +42 -4
- package/cli/lib/client-adapters.js +8 -2
- package/cli/lib/config.js +131 -21
- package/cli/run.js +4 -3
- package/cli/run.test.js +310 -12
- package/config/gx-versions.json +57 -0
- package/docs/llm_cli_mcp_playbook.md +23 -0
- package/package.json +3 -3
- package/publish/GxMcp.Gateway.deps.json +2 -2
- package/publish/GxMcp.Gateway.dll +0 -0
- package/publish/GxMcp.Gateway.exe +0 -0
- package/publish/config/gx-versions.json +57 -0
- package/publish/config.json +8 -8
- package/publish/gxmcp-manifest.json +10 -15
- package/publish/gxmcp-sbom.json +4 -4
- package/publish/tool_definitions.json +12 -12
- package/publish/worker/GxMcp.Worker.exe +0 -0
- package/publish/worker/config/gx-versions.json +57 -0
- package/publish/worker/gx-versions.json +57 -0
- package/publish/worker/sdk-compatibility.json +1 -1
- package/scripts/verify-install.js +2 -1
- package/publish/nexus-ide.vsix +0 -0
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
|
-
|
|
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 (
|
|
1044
|
+
if (fileSystem.existsSync(currentPackageGateway) && normalizeExePath(command) !== normalizeExePath(currentPackageGateway)) {
|
|
1010
1045
|
return {
|
|
1011
1046
|
...valid('present'),
|
|
1012
|
-
|
|
1013
|
-
|
|
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 (
|
|
1507
|
-
|
|
1508
|
-
|
|
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*(
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
12
|
-
|
|
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)
|