genexus-mcp 3.5.3 → 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/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' ? 5 : 1;
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
- removeTempPath(testGatewayPath, { force: true });
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 marks an Antigravity launcher from an old package cache as stale', () => {
1830
- const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-antigravity-stale-'));
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
- const oldGateway = path.join(tempRoot, 'old-cache', 'GxMcp.Gateway.exe');
2024
+ fs.mkdirSync(path.dirname(checkoutGateway), { recursive: true });
1835
2025
  fs.mkdirSync(path.dirname(currentGateway), { recursive: true });
1836
- fs.mkdirSync(path.dirname(oldGateway), { recursive: true });
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: oldGateway, args: [] } }
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, true);
1853
- assert.match(antigravity.commandStaleReason, /different package gateway/);
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
  }
@@ -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.5.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.5.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.5.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
  }
@@ -3,17 +3,17 @@
3
3
  "WorkerExecutable": "worker\\\\GxMcp.Worker.exe",
4
4
  "InstallationPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus18"
5
5
  },
6
- "Environment": {
7
- "ResolutionPolicy": "strict"
8
- },
9
6
  "Server": {
7
+ "TerseResponses": true,
10
8
  "SessionIdleTimeoutMinutes": 10,
11
- "EmitStructuredContent": false,
12
- "WorkerIdleTimeoutMinutes": 5,
13
9
  "McpStdio": true,
14
10
  "HttpPort": 0,
15
- "TerseResponses": true
11
+ "EmitStructuredContent": false,
12
+ "WorkerIdleTimeoutMinutes": 5
16
13
  },
17
14
  "ConfigSchemaVersion": 2,
15
+ "Environment": {
16
+ "ResolutionPolicy": "strict"
17
+ },
18
18
  "GatewayMode": "stdio-isolated"
19
19
  }
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": "gxmcp-release-manifest/1",
3
- "version": "3.5.3",
4
- "sourceCommit": "a4936251058358b0c5ee47745834f40c2e049073",
3
+ "version": "3.6.0",
4
+ "sourceCommit": "d884b81d07f8749afbe2a1cc2fdcd4871bb8063f",
5
5
  "sourceCommitPolicy": "exact-tag",
6
- "generatedAtUtc": "2026-09-16T02:19:28.3928043Z",
6
+ "generatedAtUtc": "2026-09-18T04:20:09.8637010Z",
7
7
  "runtime": {
8
8
  "gateway": "net10.0-windows",
9
9
  "worker": "net48-x86",
@@ -15,28 +15,28 @@
15
15
  "2026-07-28"
16
16
  ],
17
17
  "schema": "tool_definitions.json",
18
- "schemaSha256": "37db05de8cc994b9839682a94f6dce92a72e3060f8f146b91173a5f0b3e0d01d",
18
+ "schemaSha256": "2a67065b8a2986fa642a8458db556a0525f786a9cd6d8fc04ef2de3c79f4dc85",
19
19
  "provenance": "gxmcp-sbom.json",
20
20
  "artifacts": [
21
21
  {
22
22
  "path": "gxmcp-sbom.json",
23
23
  "size": 590,
24
- "sha256": "21bb6a121a14d4837a879e984e31dee26ec607876728ecb290f1784a26374e4d"
24
+ "sha256": "570adc7ca2acb00bcaea4b6baba22f2cabdb87e925272a42f006d1536014e94a"
25
25
  },
26
26
  {
27
27
  "path": "GxMcp.Gateway.exe",
28
28
  "size": 162304,
29
- "sha256": "ac8598c50a5580f2b86d497600d60d51faa5c38e81b4f01d3e6da480937acf9f"
29
+ "sha256": "235a64c3769f2ac3ba9de14d7eaac6cb9c9a0124bebf2856e6898bf4b8036f71"
30
30
  },
31
31
  {
32
32
  "path": "tool_definitions.json",
33
- "size": 116717,
34
- "sha256": "37db05de8cc994b9839682a94f6dce92a72e3060f8f146b91173a5f0b3e0d01d"
33
+ "size": 124037,
34
+ "sha256": "2a67065b8a2986fa642a8458db556a0525f786a9cd6d8fc04ef2de3c79f4dc85"
35
35
  },
36
36
  {
37
37
  "path": "worker/GxMcp.Worker.exe",
38
- "size": 3163648,
39
- "sha256": "bdb1452a05bfbe36254ae559613bc3a4b9d01ff2c63ee32c480f552077362f01"
38
+ "size": 3328000,
39
+ "sha256": "3f8efd9c36efb4dd25b888d41cb3c015334ee40a7f391e0022199321b616ff6d"
40
40
  }
41
41
  ]
42
42
  }
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "schemaVersion": "gxmcp-provenance/1",
3
- "version": "3.5.3",
4
- "sourceCommit": "a4936251058358b0c5ee47745834f40c2e049073",
3
+ "version": "3.6.0",
4
+ "sourceCommit": "d884b81d07f8749afbe2a1cc2fdcd4871bb8063f",
5
5
  "sourceCommitPolicy": "exact-tag",
6
6
  "components": [
7
7
  {
8
8
  "name": "genexus-mcp",
9
9
  "lockfile": "package-lock.json",
10
10
  "size": 39914,
11
- "sha256": "917bb4c603907353638848605f923989b02f21210fa67463aefa1deeba4a553b"
11
+ "sha256": "b204a7ddcc66a3916bff82b8cdd7185b18e433b522d5a82e2e001b27ee085e94"
12
12
  },
13
13
  {
14
14
  "name": "nexus-ide",
15
15
  "lockfile": "src/nexus-ide/package-lock.json",
16
16
  "size": 274983,
17
- "sha256": "1d1227f6e14d80bff122ff94343eb9923a0470e469e9102d28e43e0c3f103c23"
17
+ "sha256": "fdac074e28173149db6f0215fadeb7be9078ad7017bf0462d41f1f53a7ba7542"
18
18
  }
19
19
  ]
20
20
  }