genexus-mcp 3.5.3 → 3.6.1
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 +37 -17
- package/cli/commands/axi.js +128 -22
- package/cli/lib/client-adapters.js +8 -2
- package/cli/lib/config.js +163 -36
- package/cli/run.test.js +264 -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 -10
- 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/cli/run.test.js
CHANGED
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
getLauncher,
|
|
21
21
|
getGeneXusMajor,
|
|
22
22
|
getGeneXusCatalogEntries,
|
|
23
|
+
discoverGeneXusInstallation,
|
|
23
24
|
readGeneXusInstallationIdentity,
|
|
24
25
|
readGeneXusKbIdentity,
|
|
25
26
|
compareGeneXusKbAndInstallation,
|
|
@@ -39,6 +40,7 @@ if (process.platform === 'win32') {
|
|
|
39
40
|
fs.chmodSync(testGatewayPath, 0o755);
|
|
40
41
|
}
|
|
41
42
|
const testGatewayEnv = { GENEXUS_MCP_GATEWAY_EXE: testGatewayPath };
|
|
43
|
+
const testGatewayDir = path.dirname(testGatewayPath);
|
|
42
44
|
|
|
43
45
|
const RETRYABLE_REMOVE_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
44
46
|
function waitForRemoveRetry(delayMs) {
|
|
@@ -49,7 +51,7 @@ function removeTempPath(targetPath, options = {}, deps = {}) {
|
|
|
49
51
|
const fsImpl = deps.fsImpl || fs;
|
|
50
52
|
const platform = deps.platform || process.platform;
|
|
51
53
|
const sleep = deps.sleep || waitForRemoveRetry;
|
|
52
|
-
const maxAttempts = platform === 'win32' ?
|
|
54
|
+
const maxAttempts = platform === 'win32' ? 8 : 1;
|
|
53
55
|
|
|
54
56
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
55
57
|
try {
|
|
@@ -58,14 +60,38 @@ function removeTempPath(targetPath, options = {}, deps = {}) {
|
|
|
58
60
|
} catch (error) {
|
|
59
61
|
const retryable = RETRYABLE_REMOVE_CODES.has(error?.code);
|
|
60
62
|
if (!retryable || attempt === maxAttempts) throw error;
|
|
61
|
-
sleep(10 * (2 ** (attempt - 1)));
|
|
63
|
+
sleep(Math.min(10 * (2 ** (attempt - 1)), 1000));
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
// Issue #211 teardown safety net: a probe child that outlives its terminate
|
|
69
|
+
// signal keeps the stubbed GxMcp.Gateway.exe image mapped, which turns the
|
|
70
|
+
// cleanup rmSync into EPERM. Terminate only processes started from this test's
|
|
71
|
+
// own temp dir — never a machine-wide GxMcp.Gateway.exe sweep, which would hit
|
|
72
|
+
// other checkouts and the operator's own gateways.
|
|
73
|
+
function stopLingeringGatewayStubs(dirPath, deps = {}) {
|
|
74
|
+
const platform = deps.platform || process.platform;
|
|
75
|
+
const run = deps.run || ((command, args) => spawnSync(command, args, { encoding: 'utf8', windowsHide: true }));
|
|
76
|
+
if (platform !== 'win32') return false;
|
|
77
|
+
|
|
78
|
+
const escapedDir = String(dirPath).replace(/'/g, "''");
|
|
79
|
+
const script = [
|
|
80
|
+
"$ErrorActionPreference = 'SilentlyContinue'",
|
|
81
|
+
`Get-CimInstance Win32_Process -Filter "Name = 'GxMcp.Gateway.exe'"`,
|
|
82
|
+
`Where-Object { $_.ExecutablePath -and ([System.IO.Path]::GetDirectoryName($_.ExecutablePath) -eq '${escapedDir}') }`,
|
|
83
|
+
'ForEach-Object { Stop-Process -Id $_.ProcessId -Force }'
|
|
84
|
+
].join(' | ');
|
|
85
|
+
try {
|
|
86
|
+
run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]);
|
|
87
|
+
} catch { }
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
66
91
|
test.after(() => {
|
|
67
92
|
removeTempPath(testGxPath, { recursive: true, force: true });
|
|
68
|
-
|
|
93
|
+
stopLingeringGatewayStubs(testGatewayDir);
|
|
94
|
+
removeTempPath(testGatewayDir, { recursive: true, force: true });
|
|
69
95
|
});
|
|
70
96
|
|
|
71
97
|
function runCli(args, opts = {}) {
|
|
@@ -108,6 +134,58 @@ test('temporary cleanup retries transient Windows removal errors', () => {
|
|
|
108
134
|
}), permanentError);
|
|
109
135
|
});
|
|
110
136
|
|
|
137
|
+
test('windows cleanup retries keep growing the backoff up to the attempt budget', () => {
|
|
138
|
+
const sleeps = [];
|
|
139
|
+
let attempts = 0;
|
|
140
|
+
const lockedFs = {
|
|
141
|
+
rmSync() {
|
|
142
|
+
attempts += 1;
|
|
143
|
+
throw Object.assign(new Error('file is still in use'), { code: 'EBUSY' });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
assert.throws(() => removeTempPath('fixture', { force: true }, {
|
|
148
|
+
fsImpl: lockedFs,
|
|
149
|
+
platform: 'win32',
|
|
150
|
+
sleep: (delayMs) => sleeps.push(delayMs)
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
assert.equal(attempts, 8);
|
|
154
|
+
assert.deepEqual(sleeps, [10, 20, 40, 80, 160, 320, 640]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('lingering gateway stub cleanup stays scoped to the test temp dir', () => {
|
|
158
|
+
const posixCalls = [];
|
|
159
|
+
const ranOnPosix = stopLingeringGatewayStubs('C:\\Temp\\genexus-mcp-test-1', {
|
|
160
|
+
platform: 'linux',
|
|
161
|
+
run: (command, args) => posixCalls.push([command, args])
|
|
162
|
+
});
|
|
163
|
+
assert.equal(ranOnPosix, false);
|
|
164
|
+
assert.deepEqual(posixCalls, [], 'non-Windows teardown must not spawn a shell');
|
|
165
|
+
|
|
166
|
+
const windowsCalls = [];
|
|
167
|
+
const ranOnWindows = stopLingeringGatewayStubs('C:\\Temp\\genexus-mcp-test-1', {
|
|
168
|
+
platform: 'win32',
|
|
169
|
+
run: (command, args) => windowsCalls.push([command, args])
|
|
170
|
+
});
|
|
171
|
+
assert.equal(ranOnWindows, true);
|
|
172
|
+
assert.equal(windowsCalls.length, 1);
|
|
173
|
+
assert.equal(windowsCalls[0][0], 'powershell.exe');
|
|
174
|
+
|
|
175
|
+
const script = windowsCalls[0][1].join(' ');
|
|
176
|
+
assert.ok(script.includes("GxMcp.Gateway.exe"), 'must target the stubbed gateway image');
|
|
177
|
+
assert.ok(script.includes('C:\\Temp\\genexus-mcp-test-1'), 'must be scoped to the test temp dir');
|
|
178
|
+
assert.ok(!script.includes('Stop-Process -Name'), 'must never terminate by process name machine-wide');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('a failed stub cleanup does not mask the test result', () => {
|
|
182
|
+
const ran = stopLingeringGatewayStubs('C:\\Temp\\genexus-mcp-test-1', {
|
|
183
|
+
platform: 'win32',
|
|
184
|
+
run: () => { throw new Error('powershell unavailable'); }
|
|
185
|
+
});
|
|
186
|
+
assert.equal(ran, true);
|
|
187
|
+
});
|
|
188
|
+
|
|
111
189
|
test('status returns structured json envelope with schema version', () => {
|
|
112
190
|
const result = runCli(['status', '--format', 'json']);
|
|
113
191
|
assert.equal(result.status, 0);
|
|
@@ -476,6 +554,37 @@ test('GeneXus installation identity falls back to executable metadata', () => {
|
|
|
476
554
|
}
|
|
477
555
|
});
|
|
478
556
|
|
|
557
|
+
test('classic GeneXus installation identity reads gx.exe metadata', () => {
|
|
558
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-classic-version-'));
|
|
559
|
+
try {
|
|
560
|
+
fs.writeFileSync(path.join(tempRoot, 'gx.exe'), 'not-a-real-executable');
|
|
561
|
+
const identity = readGeneXusInstallationIdentity(tempRoot, {
|
|
562
|
+
readExecutableVersion: (exePath) => exePath.endsWith('gx.exe') ? '9.0.123' : null
|
|
563
|
+
});
|
|
564
|
+
assert.deepEqual(identity, {
|
|
565
|
+
version: '9.0.123',
|
|
566
|
+
major: '9',
|
|
567
|
+
source: 'executable-metadata'
|
|
568
|
+
});
|
|
569
|
+
} finally {
|
|
570
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test('classic GeneXus installation discovery accepts GENEXUS_HOME with gx.exe', () => {
|
|
575
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-classic-home-'));
|
|
576
|
+
const previousHome = process.env.GENEXUS_HOME;
|
|
577
|
+
try {
|
|
578
|
+
fs.writeFileSync(path.join(tempRoot, 'gx.exe'), 'not-a-real-executable');
|
|
579
|
+
process.env.GENEXUS_HOME = tempRoot;
|
|
580
|
+
assert.equal(discoverGeneXusInstallation(), tempRoot);
|
|
581
|
+
} finally {
|
|
582
|
+
if (previousHome === undefined) delete process.env.GENEXUS_HOME;
|
|
583
|
+
else process.env.GENEXUS_HOME = previousHome;
|
|
584
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
479
588
|
test('GeneXus installation identity ignores an invalid version file when executable metadata is valid', () => {
|
|
480
589
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-invalid-version-'));
|
|
481
590
|
try {
|
|
@@ -542,8 +651,45 @@ test('KB identity fails closed for malformed gxw metadata', () => {
|
|
|
542
651
|
|
|
543
652
|
test('catalog discovery ordering can prefer the KB major', () => {
|
|
544
653
|
assert.equal(getGeneXusMajor('17.0.11.163677'), '17');
|
|
654
|
+
assert.equal(getGeneXusMajor('10.3.0.86550'), '10.3');
|
|
655
|
+
assert.equal(getGeneXusMajor('10.2.0'), '10.2');
|
|
656
|
+
assert.equal(getGeneXusMajor('10.1.0'), '10.1');
|
|
657
|
+
assert.equal(getGeneXusMajor('9.0.123'), '9');
|
|
658
|
+
assert.equal(getGeneXusMajor('8.0.456'), '8');
|
|
545
659
|
assert.equal(getGeneXusCatalogEntries('17')[0].major, '17');
|
|
546
660
|
assert.equal(getGeneXusCatalogEntries('18')[0].major, '18');
|
|
661
|
+
assert.equal(getGeneXusCatalogEntries('10.3')[0].major, '10.3');
|
|
662
|
+
assert.equal(getGeneXusCatalogEntries('9')[0].major, '9');
|
|
663
|
+
assert.equal(getGeneXusCatalogEntries('8')[0].major, '8');
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test('KB identity reads Evolution 3 decimal major from gxw metadata', () => {
|
|
667
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-kb-ev3-'));
|
|
668
|
+
try {
|
|
669
|
+
fs.writeFileSync(
|
|
670
|
+
path.join(tempRoot, 'KnowledgeBase.gxw'),
|
|
671
|
+
'<KnowledgeBase><FriendlyVersion>GeneXus Evolution 3</FriendlyVersion><VersionNumber>10.3.0.86550</VersionNumber></KnowledgeBase>'
|
|
672
|
+
);
|
|
673
|
+
const identity = readGeneXusKbIdentity(tempRoot);
|
|
674
|
+
assert.equal(identity.version, '10.3.0.86550');
|
|
675
|
+
assert.equal(identity.major, '10.3');
|
|
676
|
+
assert.equal(identity.source, 'gxw-version');
|
|
677
|
+
} finally {
|
|
678
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
test('KB identity reads GeneXus 9.0 classic major from gxi when gxw is absent', () => {
|
|
683
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-kb-gx9-'));
|
|
684
|
+
try {
|
|
685
|
+
fs.writeFileSync(path.join(tempRoot, 'MyLegacyKb.gxi'), 'binary-or-header-data');
|
|
686
|
+
const identity = readGeneXusKbIdentity(tempRoot);
|
|
687
|
+
assert.equal(identity.major, '9');
|
|
688
|
+
assert.equal(identity.version, '9.0');
|
|
689
|
+
assert.equal(identity.source, 'gxi-classic');
|
|
690
|
+
} finally {
|
|
691
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
692
|
+
}
|
|
547
693
|
});
|
|
548
694
|
|
|
549
695
|
test('init rejects a known KB and SDK major mismatch before writing config', () => {
|
|
@@ -1062,7 +1208,43 @@ test('doctor rejects a KB and SDK major that is outside the compatibility catalo
|
|
|
1062
1208
|
assert.ok(check);
|
|
1063
1209
|
assert.equal(check.status, 'fail');
|
|
1064
1210
|
assert.match(check.detail, /KB major 19 is not supported/);
|
|
1065
|
-
assert.match(check.detail, /Supported majors: 17, 18/);
|
|
1211
|
+
assert.match(check.detail, /Supported majors: 16, 17, 18/);
|
|
1212
|
+
} finally {
|
|
1213
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
test('doctor evaluates gxpublic_com_registration and legacy_ide_lock checks for classic GeneXus', () => {
|
|
1218
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-doctor-classic-'));
|
|
1219
|
+
const kbDir = path.join(tempRoot, 'kb9');
|
|
1220
|
+
const gxDir = path.join(tempRoot, 'GeneXus9');
|
|
1221
|
+
fs.mkdirSync(kbDir, { recursive: true });
|
|
1222
|
+
fs.mkdirSync(gxDir, { recursive: true });
|
|
1223
|
+
fs.writeFileSync(path.join(kbDir, 'test.gxi'), 'legacy classic kb index');
|
|
1224
|
+
fs.writeFileSync(path.join(gxDir, 'gx.exe'), 'classic-genexus-exe');
|
|
1225
|
+
const configPath = path.join(tempRoot, 'config.json');
|
|
1226
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
1227
|
+
GeneXus: { InstallationPath: gxDir },
|
|
1228
|
+
Environment: { KBPath: kbDir }
|
|
1229
|
+
}));
|
|
1230
|
+
|
|
1231
|
+
try {
|
|
1232
|
+
const result = runCli(['doctor', '--format', 'json', '--limit', '50'], {
|
|
1233
|
+
env: {
|
|
1234
|
+
GX_CONFIG_PATH: configPath,
|
|
1235
|
+
GENEXUS_MCP_GATEWAY_EXE: process.execPath,
|
|
1236
|
+
LOCALAPPDATA: tempRoot
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
assert.equal(result.status, 0);
|
|
1240
|
+
const parsed = JSON.parse(result.stdout);
|
|
1241
|
+
const comCheck = parsed.ok.checks.find((row) => row.id === 'gxpublic_com_registration');
|
|
1242
|
+
assert.ok(comCheck, 'gxpublic_com_registration check should be present');
|
|
1243
|
+
assert.ok(['pass', 'warn'].includes(comCheck.status), `expected pass or warn, got ${comCheck.status}`);
|
|
1244
|
+
|
|
1245
|
+
const ideCheck = parsed.ok.checks.find((row) => row.id === 'legacy_ide_lock');
|
|
1246
|
+
assert.ok(ideCheck, 'legacy_ide_lock check should be present');
|
|
1247
|
+
assert.ok(['pass', 'warn'].includes(ideCheck.status), `expected pass or warn, got ${ideCheck.status}`);
|
|
1066
1248
|
} finally {
|
|
1067
1249
|
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
1068
1250
|
}
|
|
@@ -1310,6 +1492,41 @@ test('clients add patches OpenCode Desktop into shared opencode config', () => {
|
|
|
1310
1492
|
}
|
|
1311
1493
|
});
|
|
1312
1494
|
|
|
1495
|
+
test('OpenCode status and remove inspect both json and jsonc when they coexist', () => {
|
|
1496
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-opencode-dual-config-'));
|
|
1497
|
+
try {
|
|
1498
|
+
const env = sandboxHomeEnv(tempRoot);
|
|
1499
|
+
const configPath = path.join(tempRoot, 'config.json');
|
|
1500
|
+
const openCodeDir = path.join(env.XDG_CONFIG_HOME, 'opencode');
|
|
1501
|
+
const jsonPath = path.join(openCodeDir, 'opencode.json');
|
|
1502
|
+
const jsoncPath = path.join(openCodeDir, 'opencode.jsonc');
|
|
1503
|
+
fs.mkdirSync(openCodeDir, { recursive: true });
|
|
1504
|
+
fs.writeFileSync(configPath, JSON.stringify({ Environment: { KBPath: tempRoot } }));
|
|
1505
|
+
fs.writeFileSync(jsoncPath, JSON.stringify({ mcp: { servers: {} } }));
|
|
1506
|
+
fs.writeFileSync(jsonPath, JSON.stringify({ mcp: { genexus18mcp: {
|
|
1507
|
+
type: 'local',
|
|
1508
|
+
command: ['npx.cmd', '-y', 'genexus-mcp@latest']
|
|
1509
|
+
} } }));
|
|
1510
|
+
|
|
1511
|
+
const status = runCli(['clients', '--format', 'json'], {
|
|
1512
|
+
env: { ...env, GX_CONFIG_PATH: configPath }
|
|
1513
|
+
});
|
|
1514
|
+
assert.equal(status.status, 0);
|
|
1515
|
+
const row = JSON.parse(status.stdout).ok.clients.find((client) => client.id === 'opencode');
|
|
1516
|
+
assert.ok(row && row.registered);
|
|
1517
|
+
assert.equal(row.configPath, jsonPath);
|
|
1518
|
+
|
|
1519
|
+
const removed = runCli(['clients', 'remove', '--clients', 'opencode', '--format', 'json'], {
|
|
1520
|
+
env: { ...env, GX_CONFIG_PATH: configPath }
|
|
1521
|
+
});
|
|
1522
|
+
assert.equal(removed.status, 0);
|
|
1523
|
+
assert.equal(JSON.parse(fs.readFileSync(jsonPath, 'utf8')).mcp.genexus18mcp, undefined);
|
|
1524
|
+
assert.equal(JSON.parse(fs.readFileSync(jsoncPath, 'utf8')).mcp.servers.genexus18mcp, undefined);
|
|
1525
|
+
} finally {
|
|
1526
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1313
1530
|
test('OpenCode Desktop is not falsely reported as installed when only CLI config exists', () => {
|
|
1314
1531
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-opencode-desktop-undetected-'));
|
|
1315
1532
|
try {
|
|
@@ -1821,26 +2038,30 @@ test('clients list keeps an existing unrecognized launcher indeterminate', () =>
|
|
|
1821
2038
|
assert.equal(cursor.launcherStructuralState, 'indeterminate');
|
|
1822
2039
|
assert.equal(cursor.launcherSemanticState, 'unknown');
|
|
1823
2040
|
assert.equal(cursor.commandStale, false, 'unknown commands are not treated as invalid');
|
|
2041
|
+
assert.equal(cursor.launcherPathDrift, false, 'drift fields are always present in the payload');
|
|
2042
|
+
assert.equal(cursor.launcherPathDriftReason, null);
|
|
1824
2043
|
} finally {
|
|
1825
2044
|
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
1826
2045
|
}
|
|
1827
2046
|
});
|
|
1828
2047
|
|
|
1829
|
-
test('clients list
|
|
1830
|
-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-
|
|
2048
|
+
test('clients list reports a checkout gateway as path drift, not as stale', () => {
|
|
2049
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-checkout-drift-'));
|
|
1831
2050
|
try {
|
|
1832
2051
|
const env = sandboxHomeEnv(tempRoot);
|
|
2052
|
+
// A checkout install registers the client against the checkout's own
|
|
2053
|
+
// publish/ gateway while this CLI compares against its own gateway.
|
|
2054
|
+
const checkoutGateway = path.join(tempRoot, 'checkout', 'publish', 'GxMcp.Gateway.exe');
|
|
1833
2055
|
const currentGateway = path.join(tempRoot, 'current', 'GxMcp.Gateway.exe');
|
|
1834
|
-
|
|
2056
|
+
fs.mkdirSync(path.dirname(checkoutGateway), { recursive: true });
|
|
1835
2057
|
fs.mkdirSync(path.dirname(currentGateway), { recursive: true });
|
|
1836
|
-
fs.
|
|
2058
|
+
fs.writeFileSync(checkoutGateway, 'checkout');
|
|
1837
2059
|
fs.writeFileSync(currentGateway, 'current');
|
|
1838
|
-
fs.writeFileSync(oldGateway, 'old');
|
|
1839
2060
|
|
|
1840
2061
|
const antigravityCfg = path.join(tempRoot, '.gemini', 'antigravity', 'mcp_config.json');
|
|
1841
2062
|
fs.mkdirSync(path.dirname(antigravityCfg), { recursive: true });
|
|
1842
2063
|
fs.writeFileSync(antigravityCfg, JSON.stringify({
|
|
1843
|
-
mcpServers: { genexus18mcp: { command:
|
|
2064
|
+
mcpServers: { genexus18mcp: { command: checkoutGateway, args: [] } }
|
|
1844
2065
|
}, null, 2));
|
|
1845
2066
|
|
|
1846
2067
|
const result = runCli(['clients', '--format', 'json'], {
|
|
@@ -1849,8 +2070,39 @@ test('clients list marks an Antigravity launcher from an old package cache as st
|
|
|
1849
2070
|
assert.equal(result.status, 0);
|
|
1850
2071
|
const parsed = JSON.parse(result.stdout);
|
|
1851
2072
|
const antigravity = parsed.ok.clients.find((client) => client.id === 'antigravity');
|
|
1852
|
-
assert.equal(antigravity.commandStale,
|
|
1853
|
-
assert.
|
|
2073
|
+
assert.equal(antigravity.commandStale, false, 'an existing gateway from another install is not broken');
|
|
2074
|
+
assert.equal(antigravity.launcherStructuralState, 'present');
|
|
2075
|
+
assert.equal(antigravity.launcherSemanticState, 'valid');
|
|
2076
|
+
assert.equal(antigravity.launcherPathDrift, true, 'the drift is still reported');
|
|
2077
|
+
assert.match(antigravity.launcherPathDriftReason, /differs from this CLI's gateway/);
|
|
2078
|
+
assert.ok(parsed.help.some((h) => h.startsWith('Note:')), 'drift is informational, not a repair instruction');
|
|
2079
|
+
assert.ok(!parsed.help.some((h) => h.includes('missing gateway exe')), 'a valid launcher must not be listed as missing');
|
|
2080
|
+
} finally {
|
|
2081
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
test('clients list marks the same gateway as this CLI without drift', () => {
|
|
2086
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-same-gateway-'));
|
|
2087
|
+
try {
|
|
2088
|
+
const env = sandboxHomeEnv(tempRoot);
|
|
2089
|
+
const gatewayPath = path.join(tempRoot, 'publish', 'GxMcp.Gateway.exe');
|
|
2090
|
+
fs.mkdirSync(path.dirname(gatewayPath), { recursive: true });
|
|
2091
|
+
fs.writeFileSync(gatewayPath, 'gateway');
|
|
2092
|
+
const cursorCfg = path.join(tempRoot, '.cursor', 'mcp.json');
|
|
2093
|
+
fs.mkdirSync(path.dirname(cursorCfg), { recursive: true });
|
|
2094
|
+
fs.writeFileSync(cursorCfg, JSON.stringify({
|
|
2095
|
+
mcpServers: { genexus18mcp: { command: gatewayPath, args: [] } }
|
|
2096
|
+
}, null, 2));
|
|
2097
|
+
|
|
2098
|
+
const result = runCli(['clients', '--format', 'json'], {
|
|
2099
|
+
env: { ...env, GENEXUS_MCP_GATEWAY_EXE: gatewayPath }
|
|
2100
|
+
});
|
|
2101
|
+
assert.equal(result.status, 0);
|
|
2102
|
+
const cursor = JSON.parse(result.stdout).ok.clients.find((client) => client.id === 'cursor');
|
|
2103
|
+
assert.equal(cursor.commandStale, false);
|
|
2104
|
+
assert.equal(cursor.launcherPathDrift, false);
|
|
2105
|
+
assert.equal(cursor.launcherPathDriftReason, null);
|
|
1854
2106
|
} finally {
|
|
1855
2107
|
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
1856
2108
|
}
|
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.1",
|
|
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 with native SDK support; basic legacy compatibility for GeneXus Evolution 3, GeneXus Evolution 2, GeneXus Evolution 1, GeneXus 15, GeneXus 9.0, GeneXus 8.0 via com-gxpublic and dotnet-reflection drivers — 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.1": {
|
|
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.1": {
|
|
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
|
-
"GeneXus": {
|
|
3
|
-
"WorkerExecutable": "worker\\\\GxMcp.Worker.exe",
|
|
4
|
-
"InstallationPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus18"
|
|
5
|
-
},
|
|
6
2
|
"Environment": {
|
|
7
3
|
"ResolutionPolicy": "strict"
|
|
8
4
|
},
|
|
9
5
|
"Server": {
|
|
10
|
-
"SessionIdleTimeoutMinutes": 10,
|
|
11
|
-
"EmitStructuredContent": false,
|
|
12
|
-
"WorkerIdleTimeoutMinutes": 5,
|
|
13
6
|
"McpStdio": true,
|
|
7
|
+
"TerseResponses": true,
|
|
14
8
|
"HttpPort": 0,
|
|
15
|
-
"
|
|
9
|
+
"WorkerIdleTimeoutMinutes": 5,
|
|
10
|
+
"SessionIdleTimeoutMinutes": 10,
|
|
11
|
+
"EmitStructuredContent": false
|
|
16
12
|
},
|
|
17
13
|
"ConfigSchemaVersion": 2,
|
|
14
|
+
"GeneXus": {
|
|
15
|
+
"InstallationPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus18",
|
|
16
|
+
"WorkerExecutable": "worker\\\\GxMcp.Worker.exe"
|
|
17
|
+
},
|
|
18
18
|
"GatewayMode": "stdio-isolated"
|
|
19
19
|
}
|