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/run.test.js
CHANGED
|
@@ -39,6 +39,7 @@ if (process.platform === 'win32') {
|
|
|
39
39
|
fs.chmodSync(testGatewayPath, 0o755);
|
|
40
40
|
}
|
|
41
41
|
const testGatewayEnv = { GENEXUS_MCP_GATEWAY_EXE: testGatewayPath };
|
|
42
|
+
const testGatewayDir = path.dirname(testGatewayPath);
|
|
42
43
|
|
|
43
44
|
const RETRYABLE_REMOVE_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
44
45
|
function waitForRemoveRetry(delayMs) {
|
|
@@ -49,7 +50,7 @@ function removeTempPath(targetPath, options = {}, deps = {}) {
|
|
|
49
50
|
const fsImpl = deps.fsImpl || fs;
|
|
50
51
|
const platform = deps.platform || process.platform;
|
|
51
52
|
const sleep = deps.sleep || waitForRemoveRetry;
|
|
52
|
-
const maxAttempts = platform === 'win32' ?
|
|
53
|
+
const maxAttempts = platform === 'win32' ? 8 : 1;
|
|
53
54
|
|
|
54
55
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
55
56
|
try {
|
|
@@ -58,14 +59,38 @@ function removeTempPath(targetPath, options = {}, deps = {}) {
|
|
|
58
59
|
} catch (error) {
|
|
59
60
|
const retryable = RETRYABLE_REMOVE_CODES.has(error?.code);
|
|
60
61
|
if (!retryable || attempt === maxAttempts) throw error;
|
|
61
|
-
sleep(10 * (2 ** (attempt - 1)));
|
|
62
|
+
sleep(Math.min(10 * (2 ** (attempt - 1)), 1000));
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
// Issue #211 teardown safety net: a probe child that outlives its terminate
|
|
68
|
+
// signal keeps the stubbed GxMcp.Gateway.exe image mapped, which turns the
|
|
69
|
+
// cleanup rmSync into EPERM. Terminate only processes started from this test's
|
|
70
|
+
// own temp dir — never a machine-wide GxMcp.Gateway.exe sweep, which would hit
|
|
71
|
+
// other checkouts and the operator's own gateways.
|
|
72
|
+
function stopLingeringGatewayStubs(dirPath, deps = {}) {
|
|
73
|
+
const platform = deps.platform || process.platform;
|
|
74
|
+
const run = deps.run || ((command, args) => spawnSync(command, args, { encoding: 'utf8', windowsHide: true }));
|
|
75
|
+
if (platform !== 'win32') return false;
|
|
76
|
+
|
|
77
|
+
const escapedDir = String(dirPath).replace(/'/g, "''");
|
|
78
|
+
const script = [
|
|
79
|
+
"$ErrorActionPreference = 'SilentlyContinue'",
|
|
80
|
+
`Get-CimInstance Win32_Process -Filter "Name = 'GxMcp.Gateway.exe'"`,
|
|
81
|
+
`Where-Object { $_.ExecutablePath -and ([System.IO.Path]::GetDirectoryName($_.ExecutablePath) -eq '${escapedDir}') }`,
|
|
82
|
+
'ForEach-Object { Stop-Process -Id $_.ProcessId -Force }'
|
|
83
|
+
].join(' | ');
|
|
84
|
+
try {
|
|
85
|
+
run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]);
|
|
86
|
+
} catch { }
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
66
90
|
test.after(() => {
|
|
67
91
|
removeTempPath(testGxPath, { recursive: true, force: true });
|
|
68
|
-
|
|
92
|
+
stopLingeringGatewayStubs(testGatewayDir);
|
|
93
|
+
removeTempPath(testGatewayDir, { recursive: true, force: true });
|
|
69
94
|
});
|
|
70
95
|
|
|
71
96
|
function runCli(args, opts = {}) {
|
|
@@ -108,6 +133,58 @@ test('temporary cleanup retries transient Windows removal errors', () => {
|
|
|
108
133
|
}), permanentError);
|
|
109
134
|
});
|
|
110
135
|
|
|
136
|
+
test('windows cleanup retries keep growing the backoff up to the attempt budget', () => {
|
|
137
|
+
const sleeps = [];
|
|
138
|
+
let attempts = 0;
|
|
139
|
+
const lockedFs = {
|
|
140
|
+
rmSync() {
|
|
141
|
+
attempts += 1;
|
|
142
|
+
throw Object.assign(new Error('file is still in use'), { code: 'EBUSY' });
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
assert.throws(() => removeTempPath('fixture', { force: true }, {
|
|
147
|
+
fsImpl: lockedFs,
|
|
148
|
+
platform: 'win32',
|
|
149
|
+
sleep: (delayMs) => sleeps.push(delayMs)
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
assert.equal(attempts, 8);
|
|
153
|
+
assert.deepEqual(sleeps, [10, 20, 40, 80, 160, 320, 640]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('lingering gateway stub cleanup stays scoped to the test temp dir', () => {
|
|
157
|
+
const posixCalls = [];
|
|
158
|
+
const ranOnPosix = stopLingeringGatewayStubs('C:\\Temp\\genexus-mcp-test-1', {
|
|
159
|
+
platform: 'linux',
|
|
160
|
+
run: (command, args) => posixCalls.push([command, args])
|
|
161
|
+
});
|
|
162
|
+
assert.equal(ranOnPosix, false);
|
|
163
|
+
assert.deepEqual(posixCalls, [], 'non-Windows teardown must not spawn a shell');
|
|
164
|
+
|
|
165
|
+
const windowsCalls = [];
|
|
166
|
+
const ranOnWindows = stopLingeringGatewayStubs('C:\\Temp\\genexus-mcp-test-1', {
|
|
167
|
+
platform: 'win32',
|
|
168
|
+
run: (command, args) => windowsCalls.push([command, args])
|
|
169
|
+
});
|
|
170
|
+
assert.equal(ranOnWindows, true);
|
|
171
|
+
assert.equal(windowsCalls.length, 1);
|
|
172
|
+
assert.equal(windowsCalls[0][0], 'powershell.exe');
|
|
173
|
+
|
|
174
|
+
const script = windowsCalls[0][1].join(' ');
|
|
175
|
+
assert.ok(script.includes("GxMcp.Gateway.exe"), 'must target the stubbed gateway image');
|
|
176
|
+
assert.ok(script.includes('C:\\Temp\\genexus-mcp-test-1'), 'must be scoped to the test temp dir');
|
|
177
|
+
assert.ok(!script.includes('Stop-Process -Name'), 'must never terminate by process name machine-wide');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('a failed stub cleanup does not mask the test result', () => {
|
|
181
|
+
const ran = stopLingeringGatewayStubs('C:\\Temp\\genexus-mcp-test-1', {
|
|
182
|
+
platform: 'win32',
|
|
183
|
+
run: () => { throw new Error('powershell unavailable'); }
|
|
184
|
+
});
|
|
185
|
+
assert.equal(ran, true);
|
|
186
|
+
});
|
|
187
|
+
|
|
111
188
|
test('status returns structured json envelope with schema version', () => {
|
|
112
189
|
const result = runCli(['status', '--format', 'json']);
|
|
113
190
|
assert.equal(result.status, 0);
|
|
@@ -542,8 +619,45 @@ test('KB identity fails closed for malformed gxw metadata', () => {
|
|
|
542
619
|
|
|
543
620
|
test('catalog discovery ordering can prefer the KB major', () => {
|
|
544
621
|
assert.equal(getGeneXusMajor('17.0.11.163677'), '17');
|
|
622
|
+
assert.equal(getGeneXusMajor('10.3.0.86550'), '10.3');
|
|
623
|
+
assert.equal(getGeneXusMajor('10.2.0'), '10.2');
|
|
624
|
+
assert.equal(getGeneXusMajor('10.1.0'), '10.1');
|
|
625
|
+
assert.equal(getGeneXusMajor('9.0.123'), '9');
|
|
626
|
+
assert.equal(getGeneXusMajor('8.0.456'), '8');
|
|
545
627
|
assert.equal(getGeneXusCatalogEntries('17')[0].major, '17');
|
|
546
628
|
assert.equal(getGeneXusCatalogEntries('18')[0].major, '18');
|
|
629
|
+
assert.equal(getGeneXusCatalogEntries('10.3')[0].major, '10.3');
|
|
630
|
+
assert.equal(getGeneXusCatalogEntries('9')[0].major, '9');
|
|
631
|
+
assert.equal(getGeneXusCatalogEntries('8')[0].major, '8');
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test('KB identity reads Evolution 3 decimal major from gxw metadata', () => {
|
|
635
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-kb-ev3-'));
|
|
636
|
+
try {
|
|
637
|
+
fs.writeFileSync(
|
|
638
|
+
path.join(tempRoot, 'KnowledgeBase.gxw'),
|
|
639
|
+
'<KnowledgeBase><FriendlyVersion>GeneXus Evolution 3</FriendlyVersion><VersionNumber>10.3.0.86550</VersionNumber></KnowledgeBase>'
|
|
640
|
+
);
|
|
641
|
+
const identity = readGeneXusKbIdentity(tempRoot);
|
|
642
|
+
assert.equal(identity.version, '10.3.0.86550');
|
|
643
|
+
assert.equal(identity.major, '10.3');
|
|
644
|
+
assert.equal(identity.source, 'gxw-version');
|
|
645
|
+
} finally {
|
|
646
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test('KB identity reads GeneXus 9.0 classic major from gxi when gxw is absent', () => {
|
|
651
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-kb-gx9-'));
|
|
652
|
+
try {
|
|
653
|
+
fs.writeFileSync(path.join(tempRoot, 'MyLegacyKb.gxi'), 'binary-or-header-data');
|
|
654
|
+
const identity = readGeneXusKbIdentity(tempRoot);
|
|
655
|
+
assert.equal(identity.major, '9');
|
|
656
|
+
assert.equal(identity.version, '9.0');
|
|
657
|
+
assert.equal(identity.source, 'gxi-classic');
|
|
658
|
+
} finally {
|
|
659
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
660
|
+
}
|
|
547
661
|
});
|
|
548
662
|
|
|
549
663
|
test('init rejects a known KB and SDK major mismatch before writing config', () => {
|
|
@@ -1062,7 +1176,43 @@ test('doctor rejects a KB and SDK major that is outside the compatibility catalo
|
|
|
1062
1176
|
assert.ok(check);
|
|
1063
1177
|
assert.equal(check.status, 'fail');
|
|
1064
1178
|
assert.match(check.detail, /KB major 19 is not supported/);
|
|
1065
|
-
assert.match(check.detail, /Supported majors: 17, 18/);
|
|
1179
|
+
assert.match(check.detail, /Supported majors: 16, 17, 18/);
|
|
1180
|
+
} finally {
|
|
1181
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
test('doctor evaluates gxpublic_com_registration and legacy_ide_lock checks for classic GeneXus', () => {
|
|
1186
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-doctor-classic-'));
|
|
1187
|
+
const kbDir = path.join(tempRoot, 'kb9');
|
|
1188
|
+
const gxDir = path.join(tempRoot, 'GeneXus9');
|
|
1189
|
+
fs.mkdirSync(kbDir, { recursive: true });
|
|
1190
|
+
fs.mkdirSync(gxDir, { recursive: true });
|
|
1191
|
+
fs.writeFileSync(path.join(kbDir, 'test.gxi'), 'legacy classic kb index');
|
|
1192
|
+
fs.writeFileSync(path.join(gxDir, 'gx.exe'), 'classic-genexus-exe');
|
|
1193
|
+
const configPath = path.join(tempRoot, 'config.json');
|
|
1194
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
1195
|
+
GeneXus: { InstallationPath: gxDir },
|
|
1196
|
+
Environment: { KBPath: kbDir }
|
|
1197
|
+
}));
|
|
1198
|
+
|
|
1199
|
+
try {
|
|
1200
|
+
const result = runCli(['doctor', '--format', 'json', '--limit', '50'], {
|
|
1201
|
+
env: {
|
|
1202
|
+
GX_CONFIG_PATH: configPath,
|
|
1203
|
+
GENEXUS_MCP_GATEWAY_EXE: process.execPath,
|
|
1204
|
+
LOCALAPPDATA: tempRoot
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
assert.equal(result.status, 0);
|
|
1208
|
+
const parsed = JSON.parse(result.stdout);
|
|
1209
|
+
const comCheck = parsed.ok.checks.find((row) => row.id === 'gxpublic_com_registration');
|
|
1210
|
+
assert.ok(comCheck, 'gxpublic_com_registration check should be present');
|
|
1211
|
+
assert.ok(['pass', 'warn'].includes(comCheck.status), `expected pass or warn, got ${comCheck.status}`);
|
|
1212
|
+
|
|
1213
|
+
const ideCheck = parsed.ok.checks.find((row) => row.id === 'legacy_ide_lock');
|
|
1214
|
+
assert.ok(ideCheck, 'legacy_ide_lock check should be present');
|
|
1215
|
+
assert.ok(['pass', 'warn'].includes(ideCheck.status), `expected pass or warn, got ${ideCheck.status}`);
|
|
1066
1216
|
} finally {
|
|
1067
1217
|
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
1068
1218
|
}
|
|
@@ -1310,6 +1460,41 @@ test('clients add patches OpenCode Desktop into shared opencode config', () => {
|
|
|
1310
1460
|
}
|
|
1311
1461
|
});
|
|
1312
1462
|
|
|
1463
|
+
test('OpenCode status and remove inspect both json and jsonc when they coexist', () => {
|
|
1464
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-opencode-dual-config-'));
|
|
1465
|
+
try {
|
|
1466
|
+
const env = sandboxHomeEnv(tempRoot);
|
|
1467
|
+
const configPath = path.join(tempRoot, 'config.json');
|
|
1468
|
+
const openCodeDir = path.join(env.XDG_CONFIG_HOME, 'opencode');
|
|
1469
|
+
const jsonPath = path.join(openCodeDir, 'opencode.json');
|
|
1470
|
+
const jsoncPath = path.join(openCodeDir, 'opencode.jsonc');
|
|
1471
|
+
fs.mkdirSync(openCodeDir, { recursive: true });
|
|
1472
|
+
fs.writeFileSync(configPath, JSON.stringify({ Environment: { KBPath: tempRoot } }));
|
|
1473
|
+
fs.writeFileSync(jsoncPath, JSON.stringify({ mcp: { servers: {} } }));
|
|
1474
|
+
fs.writeFileSync(jsonPath, JSON.stringify({ mcp: { genexus18mcp: {
|
|
1475
|
+
type: 'local',
|
|
1476
|
+
command: ['npx.cmd', '-y', 'genexus-mcp@latest']
|
|
1477
|
+
} } }));
|
|
1478
|
+
|
|
1479
|
+
const status = runCli(['clients', '--format', 'json'], {
|
|
1480
|
+
env: { ...env, GX_CONFIG_PATH: configPath }
|
|
1481
|
+
});
|
|
1482
|
+
assert.equal(status.status, 0);
|
|
1483
|
+
const row = JSON.parse(status.stdout).ok.clients.find((client) => client.id === 'opencode');
|
|
1484
|
+
assert.ok(row && row.registered);
|
|
1485
|
+
assert.equal(row.configPath, jsonPath);
|
|
1486
|
+
|
|
1487
|
+
const removed = runCli(['clients', 'remove', '--clients', 'opencode', '--format', 'json'], {
|
|
1488
|
+
env: { ...env, GX_CONFIG_PATH: configPath }
|
|
1489
|
+
});
|
|
1490
|
+
assert.equal(removed.status, 0);
|
|
1491
|
+
assert.equal(JSON.parse(fs.readFileSync(jsonPath, 'utf8')).mcp.genexus18mcp, undefined);
|
|
1492
|
+
assert.equal(JSON.parse(fs.readFileSync(jsoncPath, 'utf8')).mcp.servers.genexus18mcp, undefined);
|
|
1493
|
+
} finally {
|
|
1494
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1313
1498
|
test('OpenCode Desktop is not falsely reported as installed when only CLI config exists', () => {
|
|
1314
1499
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-opencode-desktop-undetected-'));
|
|
1315
1500
|
try {
|
|
@@ -1821,26 +2006,30 @@ test('clients list keeps an existing unrecognized launcher indeterminate', () =>
|
|
|
1821
2006
|
assert.equal(cursor.launcherStructuralState, 'indeterminate');
|
|
1822
2007
|
assert.equal(cursor.launcherSemanticState, 'unknown');
|
|
1823
2008
|
assert.equal(cursor.commandStale, false, 'unknown commands are not treated as invalid');
|
|
2009
|
+
assert.equal(cursor.launcherPathDrift, false, 'drift fields are always present in the payload');
|
|
2010
|
+
assert.equal(cursor.launcherPathDriftReason, null);
|
|
1824
2011
|
} finally {
|
|
1825
2012
|
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
1826
2013
|
}
|
|
1827
2014
|
});
|
|
1828
2015
|
|
|
1829
|
-
test('clients list
|
|
1830
|
-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-
|
|
2016
|
+
test('clients list reports a checkout gateway as path drift, not as stale', () => {
|
|
2017
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-checkout-drift-'));
|
|
1831
2018
|
try {
|
|
1832
2019
|
const env = sandboxHomeEnv(tempRoot);
|
|
2020
|
+
// A checkout install registers the client against the checkout's own
|
|
2021
|
+
// publish/ gateway while this CLI compares against its own gateway.
|
|
2022
|
+
const checkoutGateway = path.join(tempRoot, 'checkout', 'publish', 'GxMcp.Gateway.exe');
|
|
1833
2023
|
const currentGateway = path.join(tempRoot, 'current', 'GxMcp.Gateway.exe');
|
|
1834
|
-
|
|
2024
|
+
fs.mkdirSync(path.dirname(checkoutGateway), { recursive: true });
|
|
1835
2025
|
fs.mkdirSync(path.dirname(currentGateway), { recursive: true });
|
|
1836
|
-
fs.
|
|
2026
|
+
fs.writeFileSync(checkoutGateway, 'checkout');
|
|
1837
2027
|
fs.writeFileSync(currentGateway, 'current');
|
|
1838
|
-
fs.writeFileSync(oldGateway, 'old');
|
|
1839
2028
|
|
|
1840
2029
|
const antigravityCfg = path.join(tempRoot, '.gemini', 'antigravity', 'mcp_config.json');
|
|
1841
2030
|
fs.mkdirSync(path.dirname(antigravityCfg), { recursive: true });
|
|
1842
2031
|
fs.writeFileSync(antigravityCfg, JSON.stringify({
|
|
1843
|
-
mcpServers: { genexus18mcp: { command:
|
|
2032
|
+
mcpServers: { genexus18mcp: { command: checkoutGateway, args: [] } }
|
|
1844
2033
|
}, null, 2));
|
|
1845
2034
|
|
|
1846
2035
|
const result = runCli(['clients', '--format', 'json'], {
|
|
@@ -1849,8 +2038,39 @@ test('clients list marks an Antigravity launcher from an old package cache as st
|
|
|
1849
2038
|
assert.equal(result.status, 0);
|
|
1850
2039
|
const parsed = JSON.parse(result.stdout);
|
|
1851
2040
|
const antigravity = parsed.ok.clients.find((client) => client.id === 'antigravity');
|
|
1852
|
-
assert.equal(antigravity.commandStale,
|
|
1853
|
-
assert.
|
|
2041
|
+
assert.equal(antigravity.commandStale, false, 'an existing gateway from another install is not broken');
|
|
2042
|
+
assert.equal(antigravity.launcherStructuralState, 'present');
|
|
2043
|
+
assert.equal(antigravity.launcherSemanticState, 'valid');
|
|
2044
|
+
assert.equal(antigravity.launcherPathDrift, true, 'the drift is still reported');
|
|
2045
|
+
assert.match(antigravity.launcherPathDriftReason, /differs from this CLI's gateway/);
|
|
2046
|
+
assert.ok(parsed.help.some((h) => h.startsWith('Note:')), 'drift is informational, not a repair instruction');
|
|
2047
|
+
assert.ok(!parsed.help.some((h) => h.includes('missing gateway exe')), 'a valid launcher must not be listed as missing');
|
|
2048
|
+
} finally {
|
|
2049
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
2050
|
+
}
|
|
2051
|
+
});
|
|
2052
|
+
|
|
2053
|
+
test('clients list marks the same gateway as this CLI without drift', () => {
|
|
2054
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-same-gateway-'));
|
|
2055
|
+
try {
|
|
2056
|
+
const env = sandboxHomeEnv(tempRoot);
|
|
2057
|
+
const gatewayPath = path.join(tempRoot, 'publish', 'GxMcp.Gateway.exe');
|
|
2058
|
+
fs.mkdirSync(path.dirname(gatewayPath), { recursive: true });
|
|
2059
|
+
fs.writeFileSync(gatewayPath, 'gateway');
|
|
2060
|
+
const cursorCfg = path.join(tempRoot, '.cursor', 'mcp.json');
|
|
2061
|
+
fs.mkdirSync(path.dirname(cursorCfg), { recursive: true });
|
|
2062
|
+
fs.writeFileSync(cursorCfg, JSON.stringify({
|
|
2063
|
+
mcpServers: { genexus18mcp: { command: gatewayPath, args: [] } }
|
|
2064
|
+
}, null, 2));
|
|
2065
|
+
|
|
2066
|
+
const result = runCli(['clients', '--format', 'json'], {
|
|
2067
|
+
env: { ...env, GENEXUS_MCP_GATEWAY_EXE: gatewayPath }
|
|
2068
|
+
});
|
|
2069
|
+
assert.equal(result.status, 0);
|
|
2070
|
+
const cursor = JSON.parse(result.stdout).ok.clients.find((client) => client.id === 'cursor');
|
|
2071
|
+
assert.equal(cursor.commandStale, false);
|
|
2072
|
+
assert.equal(cursor.launcherPathDrift, false);
|
|
2073
|
+
assert.equal(cursor.launcherPathDriftReason, null);
|
|
1854
2074
|
} finally {
|
|
1855
2075
|
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
1856
2076
|
}
|
|
@@ -2046,6 +2266,84 @@ test('gateway passthrough remains intact when no AXI subcommand is used', () =>
|
|
|
2046
2266
|
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
2047
2267
|
});
|
|
2048
2268
|
|
|
2269
|
+
test('parseArgs recognizes every version alias as a command token', () => {
|
|
2270
|
+
const { parseArgs } = require('./index');
|
|
2271
|
+
for (const alias of ['--version', '-v', 'version']) {
|
|
2272
|
+
const parsed = parseArgs([alias]);
|
|
2273
|
+
assert.equal(parsed.command, 'version', `${alias} must route to the version command`);
|
|
2274
|
+
assert.deepEqual(parsed.unknownFlags, [], `${alias} must not be reported as an unknown flag`);
|
|
2275
|
+
}
|
|
2276
|
+
// Version aliases are consumed as command tokens, so the remaining flags are parsed.
|
|
2277
|
+
assert.equal(parseArgs(['version', '--format', 'json']).options.format, 'json');
|
|
2278
|
+
assert.equal(parseArgs(['-v', '--format', 'json']).options.format, 'json');
|
|
2279
|
+
assert.equal(parseArgs(['--version', '--format', 'json']).options.format, 'json');
|
|
2280
|
+
// Arbitrary passthrough arguments stay in the passthrough lane.
|
|
2281
|
+
assert.equal(parseArgs(['hello', 'world']).command, null);
|
|
2282
|
+
});
|
|
2283
|
+
|
|
2284
|
+
test('version aliases print the package version without entering gateway passthrough', () => {
|
|
2285
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-version-'));
|
|
2286
|
+
const fakeConfig = path.join(tempRoot, 'config.json');
|
|
2287
|
+
fs.writeFileSync(fakeConfig, JSON.stringify({ ok: true }));
|
|
2288
|
+
|
|
2289
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
2290
|
+
// Pointing the gateway exe at the Node binary makes a regression loud: a leaked
|
|
2291
|
+
// passthrough of `--version` would print Node's version instead of the package's.
|
|
2292
|
+
const env = { GX_CONFIG_PATH: fakeConfig, GENEXUS_MCP_GATEWAY_EXE: process.execPath };
|
|
2293
|
+
|
|
2294
|
+
try {
|
|
2295
|
+
for (const alias of [['--version'], ['-v'], ['version']]) {
|
|
2296
|
+
const result = runCli(alias, { env });
|
|
2297
|
+
assert.equal(result.status, 0, `${alias.join(' ')} must exit 0`);
|
|
2298
|
+
assert.equal(result.stdout.trim(), pkg.version, `${alias.join(' ')} must print the package version`);
|
|
2299
|
+
}
|
|
2300
|
+
} finally {
|
|
2301
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
2302
|
+
}
|
|
2303
|
+
});
|
|
2304
|
+
|
|
2305
|
+
test('version --format json returns an axi-cli/1 envelope for every alias', () => {
|
|
2306
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
2307
|
+
|
|
2308
|
+
for (const args of [
|
|
2309
|
+
['version', '--format', 'json'],
|
|
2310
|
+
['-v', '--format', 'json'],
|
|
2311
|
+
['--version', '--format', 'json']
|
|
2312
|
+
]) {
|
|
2313
|
+
const result = runCli(args);
|
|
2314
|
+
assert.equal(result.status, 0, `${args.join(' ')} must exit 0`);
|
|
2315
|
+
const parsed = JSON.parse(result.stdout);
|
|
2316
|
+
assert.equal(parsed.ok.version, pkg.version);
|
|
2317
|
+
assert.equal(parsed.meta.schemaVersion, 'axi-cli/1');
|
|
2318
|
+
assert.equal(parsed.meta.command, 'version');
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
|
|
2322
|
+
test('version rejects unknown flags instead of printing a version', () => {
|
|
2323
|
+
const result = runCli(['version', '--bogus']);
|
|
2324
|
+
assert.equal(result.status, 2);
|
|
2325
|
+
assert.match(result.stdout, /Unknown flag: --bogus/);
|
|
2326
|
+
});
|
|
2327
|
+
|
|
2328
|
+
test('version --help documents the command', () => {
|
|
2329
|
+
const result = runCli(['version', '--help', '--format', 'json']);
|
|
2330
|
+
assert.equal(result.status, 0);
|
|
2331
|
+
const parsed = JSON.parse(result.stdout);
|
|
2332
|
+
assert.equal(parsed.ok.command, 'version');
|
|
2333
|
+
assert.match(parsed.ok.usage, /--version/);
|
|
2334
|
+
});
|
|
2335
|
+
|
|
2336
|
+
test('version fails loudly when the package version cannot be read', async () => {
|
|
2337
|
+
const { handleVersion } = require('./commands/axi');
|
|
2338
|
+
const result = await handleVersion({}, { EXIT_CODES: { OK: 0, ERROR: 1, USAGE: 2 } }, {
|
|
2339
|
+
getPackageVersion: () => null
|
|
2340
|
+
});
|
|
2341
|
+
|
|
2342
|
+
assert.equal(result.exitCode, 1);
|
|
2343
|
+
assert.equal(result.envelope.error.code, 'version_unavailable');
|
|
2344
|
+
assert.match(result.envelope.error.message, /package\.json/);
|
|
2345
|
+
});
|
|
2346
|
+
|
|
2049
2347
|
test('stdio launcher persists the last child stderr when the gateway exits non-zero', () => {
|
|
2050
2348
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-stdio-crash-'));
|
|
2051
2349
|
try {
|
package/config/gx-versions.json
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
"$schema": "genexus-mcp/version-catalog/1",
|
|
3
3
|
"primaryMajor": "18",
|
|
4
4
|
"supportedMajors": [
|
|
5
|
+
{
|
|
6
|
+
"major": "16",
|
|
7
|
+
"displayName": "GeneXus 16",
|
|
8
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus16",
|
|
9
|
+
"registryNames": ["GeneXus 16"],
|
|
10
|
+
"legacyRegistryVersions": ["16.0"]
|
|
11
|
+
},
|
|
5
12
|
{
|
|
6
13
|
"major": "17",
|
|
7
14
|
"displayName": "GeneXus 17",
|
|
@@ -16,5 +23,55 @@
|
|
|
16
23
|
"registryNames": ["GeneXus 18"],
|
|
17
24
|
"legacyRegistryVersions": ["18.0"]
|
|
18
25
|
}
|
|
26
|
+
],
|
|
27
|
+
"legacyMajors": [
|
|
28
|
+
{
|
|
29
|
+
"major": "10.3",
|
|
30
|
+
"displayName": "GeneXus Evolution 3",
|
|
31
|
+
"driver": "dotnet-reflection",
|
|
32
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\GeneXus\\GeneXusXEv3",
|
|
33
|
+
"registryNames": ["GeneXus X Evolution 3", "GeneXus 10.3"],
|
|
34
|
+
"legacyRegistryVersions": ["10.3"]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"major": "10.2",
|
|
38
|
+
"displayName": "GeneXus Evolution 2",
|
|
39
|
+
"driver": "dotnet-reflection",
|
|
40
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\GeneXus\\GeneXusXEv2",
|
|
41
|
+
"registryNames": ["GeneXus X Evolution 2", "GeneXus 10.2"],
|
|
42
|
+
"legacyRegistryVersions": ["10.2"]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"major": "10.1",
|
|
46
|
+
"displayName": "GeneXus Evolution 1",
|
|
47
|
+
"driver": "dotnet-reflection",
|
|
48
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\GeneXus\\GeneXusXEv1",
|
|
49
|
+
"registryNames": ["GeneXus X Evolution 1", "GeneXus 10.1"],
|
|
50
|
+
"legacyRegistryVersions": ["10.1"]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"major": "15",
|
|
54
|
+
"displayName": "GeneXus 15",
|
|
55
|
+
"driver": "dotnet-reflection",
|
|
56
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus15",
|
|
57
|
+
"registryNames": ["GeneXus 15"],
|
|
58
|
+
"legacyRegistryVersions": ["15.0"]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"major": "9",
|
|
62
|
+
"displayName": "GeneXus 9.0",
|
|
63
|
+
"driver": "com-gxpublic",
|
|
64
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 9.0",
|
|
65
|
+
"registryNames": ["GeneXus 9.0"],
|
|
66
|
+
"legacyRegistryVersions": ["9.0"]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"major": "8",
|
|
70
|
+
"displayName": "GeneXus 8.0",
|
|
71
|
+
"driver": "com-gxpublic",
|
|
72
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 8.0",
|
|
73
|
+
"registryNames": ["GeneXus 8.0"],
|
|
74
|
+
"legacyRegistryVersions": ["8.0"]
|
|
75
|
+
}
|
|
19
76
|
]
|
|
20
77
|
}
|
|
@@ -41,6 +41,7 @@ Entry points:
|
|
|
41
41
|
Neutral installer/runtime setup:
|
|
42
42
|
- `genexus-mcp config create --config-scope neutral --output <path> --gx <path> --worker <path> --gateway-mode stdio-isolated --resolution-policy strict`
|
|
43
43
|
- `genexus-mcp clients add --all-clients` (or `--clients opencode,codex-cli,vscode,antigravity,gemini-cli`)
|
|
44
|
+
- From a **local checkout**, register/repair harnesses with the checkout CLI and gateway instead of `npx`: from the repo root `$env:GENEXUS_MCP_GATEWAY_EXE='<repoRoot>\publish\GxMcp.Gateway.exe'; node cli\run.js clients add --all-clients` (or `./install.ps1`), then validate with `node cli\run.js clients` / `node cli\run.js doctor`. Reserve the `npx genexus-mcp@latest …` forms for installs that have no local `publish\`; see the startup-failure section for the stale-path case.
|
|
44
45
|
- `genexus-mcp config migrate --from <legacy.json> --output <neutral.json>` is the only configuration migration path. It copies runtime fields into the neutral schema (`ConfigSchemaVersion: 2`, `GatewayMode`, `GeneXus`, `Server`, and `Environment.ResolutionPolicy`), writes an atomic source backup, verifies the destination by read-back, and rolls back an existing destination if verification fails. The JSON receipt reports `backupPath`, `readBack`, `rolledBack`, `migrated`, and `notMigrated`.
|
|
45
46
|
- Legacy KB fields (`Environment.KBPath`, `KBs`, `DefaultKb`, `ActiveKb`) are deliberately not migrated because neutral configs are KB-free. Use `--reject-non-migratable` to fail before writing when any are present.
|
|
46
47
|
- `init` and `clients add` never migrate or rewrite an existing config automatically. `kb add`, `kb remove`, and `kb switch` remain the legacy catalog flow: they mutate the `Environment` catalog in the config selected by `GX_CONFIG_PATH` or the current directory. Neutral runtimes use explicit per-session MCP KB selection instead.
|
|
@@ -76,6 +77,28 @@ marks that path stale, re-register it with `npx genexus-mcp@latest clients add -
|
|
|
76
77
|
Do not infer an npx incompatibility or switch to a global npm install before reading
|
|
77
78
|
this file.
|
|
78
79
|
|
|
80
|
+
**Local checkout.** If you run clients from a repo checkout that has a local `publish\`,
|
|
81
|
+
re-register and validate with that same checkout CLI so the harness keeps pointing at
|
|
82
|
+
its gateway:
|
|
83
|
+
|
|
84
|
+
```powershell
|
|
85
|
+
# from the repo root
|
|
86
|
+
$env:GENEXUS_MCP_GATEWAY_EXE = "<repoRoot>\publish\GxMcp.Gateway.exe"
|
|
87
|
+
node cli\run.js clients add --clients <id> # or re-run .\install.ps1
|
|
88
|
+
node cli\run.js clients --format json # validate
|
|
89
|
+
node cli\run.js doctor --mcp-smoke --format json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`clients` and `doctor` only validate — neither rewrites a launcher. Running
|
|
93
|
+
`npx genexus-mcp@latest clients add` from the checkout (or its help line) rewrites the
|
|
94
|
+
client to the npm-cache launcher, which is what actually moves a harness off the
|
|
95
|
+
checkout; that is the reason to keep install, validation, and repair on one CLI/env.
|
|
96
|
+
Reading back a checkout registration with a different CLI is safe and informative: a
|
|
97
|
+
client that points at another *existing* gateway is reported as `launcherPathDrift`
|
|
98
|
+
with `commandStale: false`, not as stale, because the launcher is valid. `doctor`'s
|
|
99
|
+
`client_config_sync` check still warns when the configured exe is not the packaged one
|
|
100
|
+
(the broader, explicit drift check across every client).
|
|
101
|
+
|
|
79
102
|
## MCP Contract (LLM-facing)
|
|
80
103
|
|
|
81
104
|
For `tools/call`, parse `result.content[0].text` as JSON.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genexus-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"mcpName": "io.github.lennix1337/genexus",
|
|
5
|
-
"description": "GeneXus 17, GeneXus 18 MCP server — read, edit, and analyze Knowledge Base objects directly from Claude, Cursor, and other AI agents over the Model Context Protocol.",
|
|
5
|
+
"description": "GeneXus 16, GeneXus 17, GeneXus 18 MCP server — read, edit, and analyze Knowledge Base objects directly from Claude, Cursor, and other AI agents over the Model Context Protocol.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mcp",
|
|
8
8
|
"mcp-server",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"automation"
|
|
33
33
|
],
|
|
34
34
|
"scripts": {
|
|
35
|
-
"test": "node --test cli/run.test.js cli/lib/client-adapters.test.js cli/docs.test.js scripts/lint.test.js && npm run test:live-contract",
|
|
35
|
+
"test": "node --test cli/run.test.js cli/lib/client-adapters.test.js cli/docs.test.js scripts/lint.test.js scripts/verify-install.test.js && npm run test:live-contract",
|
|
36
36
|
"lint": "node scripts/lint.js",
|
|
37
37
|
"test:live-contract": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/tests/test-live.test.ps1",
|
|
38
38
|
"test:integration": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/integration-preflight.ps1",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"targets": {
|
|
8
8
|
".NETCoreApp,Version=v10.0": {},
|
|
9
9
|
".NETCoreApp,Version=v10.0/win-x64": {
|
|
10
|
-
"GxMcp.Gateway/3.
|
|
10
|
+
"GxMcp.Gateway/3.6.0": {
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"Newtonsoft.Json": "13.0.3",
|
|
13
13
|
"System.Management": "10.0.5",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
"libraries": {
|
|
69
|
-
"GxMcp.Gateway/3.
|
|
69
|
+
"GxMcp.Gateway/3.6.0": {
|
|
70
70
|
"type": "project",
|
|
71
71
|
"serviceable": false,
|
|
72
72
|
"sha512": ""
|
|
Binary file
|
|
Binary file
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
"$schema": "genexus-mcp/version-catalog/1",
|
|
3
3
|
"primaryMajor": "18",
|
|
4
4
|
"supportedMajors": [
|
|
5
|
+
{
|
|
6
|
+
"major": "16",
|
|
7
|
+
"displayName": "GeneXus 16",
|
|
8
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus16",
|
|
9
|
+
"registryNames": ["GeneXus 16"],
|
|
10
|
+
"legacyRegistryVersions": ["16.0"]
|
|
11
|
+
},
|
|
5
12
|
{
|
|
6
13
|
"major": "17",
|
|
7
14
|
"displayName": "GeneXus 17",
|
|
@@ -16,5 +23,55 @@
|
|
|
16
23
|
"registryNames": ["GeneXus 18"],
|
|
17
24
|
"legacyRegistryVersions": ["18.0"]
|
|
18
25
|
}
|
|
26
|
+
],
|
|
27
|
+
"legacyMajors": [
|
|
28
|
+
{
|
|
29
|
+
"major": "10.3",
|
|
30
|
+
"displayName": "GeneXus Evolution 3",
|
|
31
|
+
"driver": "dotnet-reflection",
|
|
32
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\GeneXus\\GeneXusXEv3",
|
|
33
|
+
"registryNames": ["GeneXus X Evolution 3", "GeneXus 10.3"],
|
|
34
|
+
"legacyRegistryVersions": ["10.3"]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"major": "10.2",
|
|
38
|
+
"displayName": "GeneXus Evolution 2",
|
|
39
|
+
"driver": "dotnet-reflection",
|
|
40
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\GeneXus\\GeneXusXEv2",
|
|
41
|
+
"registryNames": ["GeneXus X Evolution 2", "GeneXus 10.2"],
|
|
42
|
+
"legacyRegistryVersions": ["10.2"]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"major": "10.1",
|
|
46
|
+
"displayName": "GeneXus Evolution 1",
|
|
47
|
+
"driver": "dotnet-reflection",
|
|
48
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\GeneXus\\GeneXusXEv1",
|
|
49
|
+
"registryNames": ["GeneXus X Evolution 1", "GeneXus 10.1"],
|
|
50
|
+
"legacyRegistryVersions": ["10.1"]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"major": "15",
|
|
54
|
+
"displayName": "GeneXus 15",
|
|
55
|
+
"driver": "dotnet-reflection",
|
|
56
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus15",
|
|
57
|
+
"registryNames": ["GeneXus 15"],
|
|
58
|
+
"legacyRegistryVersions": ["15.0"]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"major": "9",
|
|
62
|
+
"displayName": "GeneXus 9.0",
|
|
63
|
+
"driver": "com-gxpublic",
|
|
64
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 9.0",
|
|
65
|
+
"registryNames": ["GeneXus 9.0"],
|
|
66
|
+
"legacyRegistryVersions": ["9.0"]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"major": "8",
|
|
70
|
+
"displayName": "GeneXus 8.0",
|
|
71
|
+
"driver": "com-gxpublic",
|
|
72
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 8.0",
|
|
73
|
+
"registryNames": ["GeneXus 8.0"],
|
|
74
|
+
"legacyRegistryVersions": ["8.0"]
|
|
75
|
+
}
|
|
19
76
|
]
|
|
20
77
|
}
|
package/publish/config.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
"Environment": {
|
|
3
|
-
"ResolutionPolicy": "strict"
|
|
4
|
-
},
|
|
5
2
|
"GeneXus": {
|
|
6
3
|
"WorkerExecutable": "worker\\\\GxMcp.Worker.exe",
|
|
7
4
|
"InstallationPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus18"
|
|
8
5
|
},
|
|
9
|
-
"ConfigSchemaVersion": 2,
|
|
10
6
|
"Server": {
|
|
11
|
-
"SessionIdleTimeoutMinutes": 10,
|
|
12
|
-
"HttpPort": 0,
|
|
13
7
|
"TerseResponses": true,
|
|
8
|
+
"SessionIdleTimeoutMinutes": 10,
|
|
14
9
|
"McpStdio": true,
|
|
15
|
-
"
|
|
16
|
-
"EmitStructuredContent": false
|
|
10
|
+
"HttpPort": 0,
|
|
11
|
+
"EmitStructuredContent": false,
|
|
12
|
+
"WorkerIdleTimeoutMinutes": 5
|
|
13
|
+
},
|
|
14
|
+
"ConfigSchemaVersion": 2,
|
|
15
|
+
"Environment": {
|
|
16
|
+
"ResolutionPolicy": "strict"
|
|
17
17
|
},
|
|
18
18
|
"GatewayMode": "stdio-isolated"
|
|
19
19
|
}
|