genexus-mcp 3.6.0 → 3.7.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.
Files changed (34) hide show
  1. package/README.md +136 -24
  2. package/cli/commands/axi.js +311 -64
  3. package/cli/lib/config.js +129 -32
  4. package/cli/run.test.js +258 -0
  5. package/config/gx-versions.json +5 -3
  6. package/package.json +2 -2
  7. package/publish/GxMcp.Gateway.deps.json +2 -2
  8. package/publish/GxMcp.Gateway.dll +0 -0
  9. package/publish/GxMcp.Gateway.exe +0 -0
  10. package/publish/config/gx-versions.json +5 -3
  11. package/publish/config.json +6 -6
  12. package/publish/gxmcp-manifest.json +10 -10
  13. package/publish/gxmcp-sbom.json +4 -4
  14. package/publish/tool_definitions.json +5 -5
  15. package/publish/worker/GxMcp.Worker.exe +0 -0
  16. package/publish/worker/GxMcp.Worker.exe.config +12 -0
  17. package/publish/worker/Microsoft.Bcl.AsyncInterfaces.dll +0 -0
  18. package/publish/worker/Microsoft.Bcl.HashCode.dll +0 -0
  19. package/publish/worker/Microsoft.Extensions.DependencyInjection.Abstractions.dll +0 -0
  20. package/publish/worker/Microsoft.Extensions.Logging.Abstractions.dll +0 -0
  21. package/publish/worker/Npgsql.dll +0 -0
  22. package/publish/worker/System.Buffers.dll +0 -0
  23. package/publish/worker/System.Collections.Immutable.dll +0 -0
  24. package/publish/worker/System.Diagnostics.DiagnosticSource.dll +0 -0
  25. package/publish/worker/System.Memory.dll +0 -0
  26. package/publish/worker/System.Numerics.Vectors.dll +0 -0
  27. package/publish/worker/System.Runtime.CompilerServices.Unsafe.dll +0 -0
  28. package/publish/worker/System.Text.Encodings.Web.dll +0 -0
  29. package/publish/worker/System.Text.Json.dll +0 -0
  30. package/publish/worker/System.Threading.Channels.dll +0 -0
  31. package/publish/worker/System.Threading.Tasks.Extensions.dll +0 -0
  32. package/publish/worker/System.ValueTuple.dll +0 -0
  33. package/publish/worker/config/gx-versions.json +5 -3
  34. package/publish/worker/gx-versions.json +5 -3
package/cli/lib/config.js CHANGED
@@ -33,6 +33,7 @@ function generateNeutralConfig(gxPath, { workerPath, gatewayMode = 'stdio-isolat
33
33
  Server: {
34
34
  HttpPort: 0,
35
35
  McpStdio: true,
36
+ WorkerSharingMode: 'isolated',
36
37
  SessionIdleTimeoutMinutes: 10,
37
38
  WorkerIdleTimeoutMinutes: 5,
38
39
  EmitStructuredContent: false,
@@ -70,6 +71,10 @@ function getToolDefinitionsPath() {
70
71
  // 3. Fallback alongside the CLI itself (defensive — for unusual layouts).
71
72
  path.join(__dirname, '..', '..', 'publish', 'tool_definitions.json')
72
73
  ];
74
+ // An explicit gateway override is usually a fixed-path or checkout install.
75
+ // Do not hide a missing sibling artifact with this source tree's copy: the
76
+ // Gateway loads the file next to its own executable at runtime.
77
+ if (process.env.GENEXUS_MCP_GATEWAY_EXE) return candidates[0];
73
78
  for (const candidate of candidates) {
74
79
  if (fs.existsSync(candidate)) return candidate;
75
80
  }
@@ -107,12 +112,17 @@ function getGeneXusVersionCatalog() {
107
112
  legacyMajors: [
108
113
  { major: '10.3', displayName: 'GeneXus Evolution 3', defaultInstallPath: 'C:\\Program Files (x86)\\GeneXus\\GeneXusXEv3' },
109
114
  { major: '9', displayName: 'GeneXus 9.0', defaultInstallPath: 'C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 9.0' },
110
- { major: '8', displayName: 'GeneXus 8.0', defaultInstallPath: 'C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 8.0' }
115
+ { major: '8', displayName: 'GeneXus 8.0', defaultInstallPath: 'C:\\Program Files (x86)\\ARTech\\GeneXus\\gxw80' }
111
116
  ],
112
117
  source: 'built-in-fallback'
113
118
  };
114
119
  }
115
120
 
121
+ function hasGeneXusExecutable(gxPath) {
122
+ return Boolean(gxPath) && ['genexus.exe', 'gx.exe', 'gxw32.exe']
123
+ .some((name) => fs.existsSync(path.join(gxPath, name)));
124
+ }
125
+
116
126
  function getGeneXusCatalogEntries(preferredMajor = null) {
117
127
  const catalog = getGeneXusVersionCatalog();
118
128
  const all = [...catalog.supportedMajors, ...(catalog.legacyMajors || [])];
@@ -153,13 +163,31 @@ function discoverGeneXusFromRegistry(preferredMajor = null) {
153
163
  const match = out.match(/InstallationDirectory\s+REG_SZ\s+(.+?)\r?\n/i);
154
164
  if (match) {
155
165
  const candidate = match[1].trim().replace(/[\\/]+$/, '');
156
- if (candidate && fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
166
+ if (candidate && hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
157
167
  return candidate;
158
168
  }
159
169
  }
160
170
  } catch {
161
171
  }
162
172
  }
173
+ // Classic ARTech installers store GX8/GX9 under Setup\\<version>
174
+ // and expose the actual IDE root in the Install value rather
175
+ // than the modern GeneXus <major>\\InstallationDirectory key.
176
+ for (const setupVersion of (entry.legacySetupVersions || [])) {
177
+ const key = `${hive}\\\\${setupVersion}`;
178
+ try {
179
+ const out = execFileSync('reg.exe', ['query', key, '/v', 'Install'], {
180
+ encoding: 'utf8',
181
+ stdio: ['ignore', 'pipe', 'ignore'],
182
+ windowsHide: true,
183
+ timeout: 3000
184
+ });
185
+ const installLine = out.split(String.fromCharCode(10)).map((line) => line.trim()).find((line) => /^Install\s+REG_SZ\s+/i.test(line));
186
+ const candidate = installLine ? installLine.replace(/^Install\s+REG_SZ\s+/i, '').trim() : null;
187
+ if (candidate && hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) return candidate;
188
+ } catch {
189
+ }
190
+ }
163
191
  for (const legacyVersion of (entry.legacyRegistryVersions || [])) {
164
192
  const key = `${hive}\\GeneXus\\${legacyVersion}`;
165
193
  try {
@@ -172,7 +200,7 @@ function discoverGeneXusFromRegistry(preferredMajor = null) {
172
200
  const match = out.match(/InstallPath\s+REG_SZ\s+(.+?)\r?\n/i);
173
201
  if (match) {
174
202
  const candidate = match[1].trim().replace(/[\\/]+$/, '');
175
- if (candidate && fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) return candidate;
203
+ if (candidate && hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) return candidate;
176
204
  }
177
205
  } catch {
178
206
  }
@@ -187,7 +215,7 @@ function discoverGeneXusFromRegistry(preferredMajor = null) {
187
215
  function discoverGeneXusInstallation(preferredMajor = null) {
188
216
  if (process.env.GENEXUS_HOME) {
189
217
  const candidate = process.env.GENEXUS_HOME.replace(/[\\/]+$/, '');
190
- if (fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) return candidate;
218
+ if (hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) return candidate;
191
219
  }
192
220
 
193
221
  const fromRegistry = discoverGeneXusFromRegistry(preferredMajor);
@@ -205,7 +233,11 @@ function discoverGeneXusInstallation(preferredMajor = null) {
205
233
  const entries = getGeneXusCatalogEntries(preferredMajor);
206
234
  const seen = new Set();
207
235
  for (const base of programDirs) {
208
- const root = path.join(base, 'GeneXus');
236
+ const roots = [
237
+ path.join(base, 'GeneXus'),
238
+ path.join(base, 'Artech', 'GeneXus')
239
+ ];
240
+ for (const root of roots) {
209
241
  const key = root.toLowerCase();
210
242
  if (seen.has(key)) continue;
211
243
  seen.add(key);
@@ -217,7 +249,7 @@ function discoverGeneXusInstallation(preferredMajor = null) {
217
249
  for (const ver of candidateNames) {
218
250
  if (!ver) continue;
219
251
  const candidate = path.join(root, ver);
220
- if (fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
252
+ if (hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
221
253
  return candidate;
222
254
  }
223
255
  }
@@ -230,13 +262,14 @@ function discoverGeneXusInstallation(preferredMajor = null) {
230
262
  const majorMatch = entry.match(/^GeneXus\s*(\d+)/i);
231
263
  if (majorMatch && !entries.some((item) => String(item.major) === majorMatch[1])) continue;
232
264
  const candidate = path.join(root, entry);
233
- if (fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
265
+ if (hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
234
266
  return candidate;
235
267
  }
236
268
  }
237
269
  }
238
270
  } catch {
239
271
  }
272
+ }
240
273
  }
241
274
 
242
275
  const fromPath = discoverGeneXusFromPath(preferredMajor);
@@ -249,15 +282,22 @@ function discoverGeneXusFromPath(preferredMajor = null) {
249
282
  if (process.platform !== 'win32') return null;
250
283
  try {
251
284
  const { execFileSync } = require('child_process');
252
- const out = execFileSync('where.exe', ['genexus.exe'], {
253
- encoding: 'utf8',
254
- stdio: ['ignore', 'pipe', 'ignore'],
255
- windowsHide: true,
256
- timeout: 3000
257
- });
258
- const first = out.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
259
- if (first && fs.existsSync(first) && matchesPreferredGeneXusMajor(path.dirname(first), preferredMajor)) {
260
- return path.dirname(first);
285
+ for (const executable of ['genexus.exe', 'gx.exe', 'gxw32.exe']) {
286
+ let out;
287
+ try {
288
+ out = execFileSync('where.exe', [executable], {
289
+ encoding: 'utf8',
290
+ stdio: ['ignore', 'pipe', 'ignore'],
291
+ windowsHide: true,
292
+ timeout: 3000
293
+ });
294
+ } catch {
295
+ continue;
296
+ }
297
+ const first = out.split('\n').map((s) => s.trim()).find(Boolean);
298
+ if (first && fs.existsSync(first) && matchesPreferredGeneXusMajor(path.dirname(first), preferredMajor)) {
299
+ return path.dirname(first);
300
+ }
261
301
  }
262
302
  } catch {
263
303
  }
@@ -344,7 +384,9 @@ function discoverKnowledgeBases(cwd, { maxResults = 25, scanDepth = 2 } = {}) {
344
384
  function directoryLooksLikeKnowledgeBase(dir) {
345
385
  try {
346
386
  const files = fs.readdirSync(dir);
347
- return files.some((f) => f.toLowerCase().endsWith('.gxw') || f.toLowerCase() === 'knowledgebase.connection');
387
+ if (files.some((f) => f.toLowerCase().endsWith('.gxw') || f.toLowerCase() === 'knowledgebase.connection')) return true;
388
+ const classicMarkers = new Set(['data001', 'gxspc001', 'kbdata', 'attribut.dat', 'att.xpw', 'objects.dat', 'objects.idx']);
389
+ return files.filter((f) => classicMarkers.has(f.toLowerCase())).length >= 2;
348
390
  } catch {
349
391
  return false;
350
392
  }
@@ -586,8 +628,11 @@ function migrateLegacyConfig(sourcePath, targetPath, { rejectNonMigratable = fal
586
628
 
587
629
  function resolveConfigPathNoMutate(cwd) {
588
630
  const cwdConfigPath = path.join(cwd, 'config.json');
589
- if (process.env.GX_CONFIG_PATH && fs.existsSync(process.env.GX_CONFIG_PATH)) {
590
- return process.env.GX_CONFIG_PATH;
631
+ // Match the Gateway: an explicit GX_CONFIG_PATH is authoritative even when
632
+ // the file is missing, so diagnostics do not silently inspect an unrelated
633
+ // cwd config while the runtime fails on the explicit path.
634
+ if (process.env.GX_CONFIG_PATH) {
635
+ return path.resolve(process.env.GX_CONFIG_PATH);
591
636
  }
592
637
  if (fs.existsSync(cwdConfigPath)) {
593
638
  return cwdConfigPath;
@@ -1910,6 +1955,9 @@ function readGeneXusInstallationIdentity(gxPath, options = {}) {
1910
1955
  if (!executableVersion) {
1911
1956
  executableVersion = readExecutableVersion(path.join(gxPath, 'gx.exe'));
1912
1957
  }
1958
+ if (!executableVersion) {
1959
+ executableVersion = readExecutableVersion(path.join(gxPath, 'gxw32.exe'));
1960
+ }
1913
1961
  if (executableVersion) {
1914
1962
  return {
1915
1963
  version: executableVersion,
@@ -1996,6 +2044,7 @@ function readGeneXusKbIdentity(kbPath) {
1996
2044
 
1997
2045
  let gxwFiles;
1998
2046
  let gxiFiles = [];
2047
+ let classicMarkerCount = 0;
1999
2048
  try {
2000
2049
  const allFiles = fs.readdirSync(kbPath);
2001
2050
  gxwFiles = allFiles
@@ -2004,6 +2053,8 @@ function readGeneXusKbIdentity(kbPath) {
2004
2053
  gxiFiles = allFiles
2005
2054
  .filter((fileName) => fileName.toLowerCase().endsWith('.gxi'))
2006
2055
  .map((fileName) => path.join(kbPath, fileName));
2056
+ const classicMarkers = new Set(['data001', 'gxspc001', 'kbdata', 'attribut.dat', 'att.xpw', 'objects.dat', 'objects.idx']);
2057
+ classicMarkerCount = allFiles.filter((fileName) => classicMarkers.has(fileName.toLowerCase())).length;
2007
2058
  } catch {
2008
2059
  return { version: null, major: null, source: 'unavailable', reason: 'unreadable-kb-path' };
2009
2060
  }
@@ -2011,6 +2062,9 @@ function readGeneXusKbIdentity(kbPath) {
2011
2062
  if (gxiFiles.length > 0) {
2012
2063
  return { version: '9.0', major: '9', source: 'gxi-classic', reason: null };
2013
2064
  }
2065
+ if (classicMarkerCount >= 2) {
2066
+ return { version: null, major: null, source: 'classic-dat', reason: 'classic-generation-requires-provider' };
2067
+ }
2014
2068
  return { version: null, major: null, source: 'unavailable', reason: 'no-gxw' };
2015
2069
  }
2016
2070
  if (gxwFiles.length > 1) return { version: null, major: null, source: 'unavailable', reason: 'multiple-gxw' };
@@ -2032,34 +2086,65 @@ function readGeneXusKbIdentity(kbPath) {
2032
2086
  }
2033
2087
  }
2034
2088
 
2035
- function compareGeneXusKbAndInstallation(kbPath, gxPath) {
2089
+ function compareGeneXusKbAndInstallation(kbPath, gxPath, options = {}) {
2036
2090
  const kb = readGeneXusKbIdentity(kbPath);
2037
- const gx = readGeneXusInstallationIdentity(gxPath);
2091
+ const gx = readGeneXusInstallationIdentity(gxPath, options);
2038
2092
  let status = 'unresolved';
2039
- if (kb.major && gx.major) status = kb.major === gx.major ? 'match' : 'mismatch';
2093
+ // A classic .gxi is shared by the GX8/GX9 family and does not carry a
2094
+ // reliable generation marker. Do not turn the historical default of 9
2095
+ // into a false GX8-vs-GX9 mismatch; the provider open is the authority.
2096
+ if ((kb.source === 'gxi-classic' || kb.source === 'classic-dat') && (gx.major === '8' || gx.major === '9')) {
2097
+ status = 'unresolved';
2098
+ } else if (kb.major && gx.major) {
2099
+ status = kb.major === gx.major ? 'match' : 'mismatch';
2100
+ }
2040
2101
  return { status, kb, gx };
2041
2102
  }
2042
2103
 
2043
2104
  function normalizeKbCatalog(raw) {
2105
+ const normalized = {};
2106
+ const entries = {};
2107
+ const add = (name, value) => {
2108
+ if (typeof name !== 'string' || !name) return;
2109
+ if (typeof value === 'string' && value) {
2110
+ normalized[name] = value;
2111
+ return;
2112
+ }
2113
+ if (!value || typeof value !== 'object') return;
2114
+ const kbPath = typeof value.path === 'string' ? value.path : value.Path;
2115
+ if (typeof kbPath !== 'string' || !kbPath) return;
2116
+ normalized[name] = kbPath;
2117
+ const metadata = { alias: name, path: kbPath };
2118
+ let hasLegacyMetadata = false;
2119
+ for (const [source, target] of [['driver', 'driver'], ['Driver', 'driver'], ['installationPath', 'installationPath'], ['InstallationPath', 'installationPath'], ['major', 'major'], ['Major', 'major']]) {
2120
+ if (typeof value[source] === 'string' && value[source]) {
2121
+ metadata[target] = value[source];
2122
+ hasLegacyMetadata = true;
2123
+ }
2124
+ }
2125
+ if (hasLegacyMetadata) entries[name] = metadata;
2126
+ };
2127
+
2044
2128
  if (Array.isArray(raw)) {
2045
- const normalized = {};
2046
2129
  for (const entry of raw) {
2047
2130
  if (!entry || typeof entry !== 'object') continue;
2048
2131
  const name = typeof entry.alias === 'string' ? entry.alias : entry.Alias;
2049
- const kbPath = typeof entry.path === 'string' ? entry.path : entry.Path;
2050
- if (typeof name === 'string' && name && typeof kbPath === 'string' && kbPath) {
2051
- normalized[name] = kbPath;
2052
- }
2132
+ add(name, entry);
2053
2133
  }
2134
+ Object.defineProperty(normalized, '__entries', { value: entries, enumerable: false, writable: true });
2054
2135
  return normalized;
2055
2136
  }
2056
2137
 
2057
- return raw && typeof raw === 'object' ? raw : {};
2138
+ if (raw && typeof raw === 'object') {
2139
+ for (const [name, value] of Object.entries(raw)) add(name, value);
2140
+ }
2141
+ Object.defineProperty(normalized, '__entries', { value: entries, enumerable: false, writable: true });
2142
+ return normalized;
2058
2143
  }
2059
2144
 
2060
- function readKbCatalog(configPath) {
2061
- if (!configPath) return { kbs: {}, activeKb: null, kbPath: null };
2062
- const cfg = readJsonFileSafe(configPath);
2145
+ function readKbCatalog(configPath, configOverride = undefined) {
2146
+ if (!configPath && configOverride === undefined) return { kbs: {}, activeKb: null, kbPath: null };
2147
+ const cfg = configOverride === undefined ? readJsonFileSafe(configPath) : configOverride;
2063
2148
  if (!cfg) return { kbs: {}, activeKb: null, kbPath: null };
2064
2149
  const env = cfg.Environment || {};
2065
2150
  const activeKb = typeof env.ActiveKb === 'string' && env.ActiveKb
@@ -2075,7 +2160,19 @@ function readKbCatalog(configPath) {
2075
2160
  function writeKbCatalog(configPath, { kbs, activeKb, kbPath }) {
2076
2161
  const cfg = readJsonFileSafe(configPath) || {};
2077
2162
  cfg.Environment = cfg.Environment || {};
2078
- cfg.Environment.KBs = kbs;
2163
+ const serializedKbs = {};
2164
+ for (const [name, kbPathValue] of Object.entries(kbs || {})) {
2165
+ const metadata = kbs.__entries && kbs.__entries[name];
2166
+ serializedKbs[name] = metadata
2167
+ ? {
2168
+ Path: kbPathValue,
2169
+ ...(metadata.driver ? { Driver: metadata.driver } : {}),
2170
+ ...(metadata.installationPath ? { InstallationPath: metadata.installationPath } : {}),
2171
+ ...(metadata.major ? { Major: metadata.major } : {})
2172
+ }
2173
+ : kbPathValue;
2174
+ }
2175
+ cfg.Environment.KBs = serializedKbs;
2079
2176
  if (activeKb) {
2080
2177
  cfg.Environment.ActiveKb = activeKb;
2081
2178
  cfg.Environment.DefaultKb = activeKb;
package/cli/run.test.js CHANGED
@@ -20,9 +20,13 @@ const {
20
20
  getLauncher,
21
21
  getGeneXusMajor,
22
22
  getGeneXusCatalogEntries,
23
+ discoverGeneXusInstallation,
23
24
  readGeneXusInstallationIdentity,
25
+ directoryLooksLikeKnowledgeBase,
24
26
  readGeneXusKbIdentity,
25
27
  compareGeneXusKbAndInstallation,
28
+ readKbCatalog,
29
+ switchActiveKb,
26
30
  patchClientConfig
27
31
  } = require('./lib/config');
28
32
  const { handleInit, resolveMcpSmokeTarget } = require('./commands/axi');
@@ -553,6 +557,54 @@ test('GeneXus installation identity falls back to executable metadata', () => {
553
557
  }
554
558
  });
555
559
 
560
+ test('classic GeneXus installation identity reads gx.exe metadata', () => {
561
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-classic-version-'));
562
+ try {
563
+ fs.writeFileSync(path.join(tempRoot, 'gx.exe'), 'not-a-real-executable');
564
+ const identity = readGeneXusInstallationIdentity(tempRoot, {
565
+ readExecutableVersion: (exePath) => exePath.endsWith('gx.exe') ? '9.0.123' : null
566
+ });
567
+ assert.deepEqual(identity, {
568
+ version: '9.0.123',
569
+ major: '9',
570
+ source: 'executable-metadata'
571
+ });
572
+ } finally {
573
+ removeTempPath(tempRoot, { recursive: true, force: true });
574
+ }
575
+ });
576
+
577
+ test('GeneXus 8 installation identity reads gxw32.exe metadata', () => {
578
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-gx8-version-'));
579
+ try {
580
+ fs.writeFileSync(path.join(tempRoot, 'gxw32.exe'), 'not-a-real-executable');
581
+ const identity = readGeneXusInstallationIdentity(tempRoot, {
582
+ readExecutableVersion: (exePath) => exePath.endsWith('gxw32.exe') ? '8.0.0.632' : null
583
+ });
584
+ assert.deepEqual(identity, {
585
+ version: '8.0.0.632',
586
+ major: '8',
587
+ source: 'executable-metadata'
588
+ });
589
+ } finally {
590
+ removeTempPath(tempRoot, { recursive: true, force: true });
591
+ }
592
+ });
593
+
594
+ test('classic GeneXus installation discovery accepts GENEXUS_HOME with gx.exe', () => {
595
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-classic-home-'));
596
+ const previousHome = process.env.GENEXUS_HOME;
597
+ try {
598
+ fs.writeFileSync(path.join(tempRoot, 'gx.exe'), 'not-a-real-executable');
599
+ process.env.GENEXUS_HOME = tempRoot;
600
+ assert.equal(discoverGeneXusInstallation(), tempRoot);
601
+ } finally {
602
+ if (previousHome === undefined) delete process.env.GENEXUS_HOME;
603
+ else process.env.GENEXUS_HOME = previousHome;
604
+ removeTempPath(tempRoot, { recursive: true, force: true });
605
+ }
606
+ });
607
+
556
608
  test('GeneXus installation identity ignores an invalid version file when executable metadata is valid', () => {
557
609
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-invalid-version-'));
558
610
  try {
@@ -660,6 +712,72 @@ test('KB identity reads GeneXus 9.0 classic major from gxi when gxw is absent',
660
712
  }
661
713
  });
662
714
 
715
+ test('classic DAT KB identity recognizes GX8/9 model markers without inventing a major', () => {
716
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-kb-classic-dat-'));
717
+ try {
718
+ for (const marker of ['DATA001', 'GXSPC001', 'ATTRIBUT.DAT', 'ATT.XPW']) {
719
+ fs.writeFileSync(path.join(tempRoot, marker), 'classic');
720
+ }
721
+ const identity = readGeneXusKbIdentity(tempRoot);
722
+ assert.equal(identity.major, null);
723
+ assert.equal(identity.source, 'classic-dat');
724
+ assert.equal(identity.reason, 'classic-generation-requires-provider');
725
+ assert.equal(directoryLooksLikeKnowledgeBase(tempRoot), true);
726
+ } finally {
727
+ removeTempPath(tempRoot, { recursive: true, force: true });
728
+ }
729
+ });
730
+
731
+ test('KB catalog edits preserve per-KB legacy driver metadata', () => {
732
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-kb-catalog-'));
733
+ const configPath = path.join(tempRoot, 'config.json');
734
+ try {
735
+ fs.writeFileSync(configPath, JSON.stringify({
736
+ Environment: {
737
+ KBs: {
738
+ sect80: {
739
+ Path: 'D:\\\\GX80\\\\SECT',
740
+ Driver: 'com-gxpublic',
741
+ InstallationPath: 'C:\\\\gxw80',
742
+ Major: '8'
743
+ }
744
+ },
745
+ ActiveKb: 'sect80'
746
+ }
747
+ }));
748
+
749
+ const catalog = readKbCatalog(configPath);
750
+ assert.equal(catalog.kbs.sect80, 'D:\\\\GX80\\\\SECT');
751
+ assert.equal(Object.keys(catalog.kbs).length, 1);
752
+ const switched = switchActiveKb(configPath, { name: 'sect80' });
753
+ assert.equal(switched.ok, true);
754
+ const persisted = JSON.parse(fs.readFileSync(configPath, 'utf8'));
755
+ assert.equal(persisted.Environment.KBs.sect80.Driver, 'com-gxpublic');
756
+ assert.equal(persisted.Environment.KBs.sect80.InstallationPath, 'C:\\\\gxw80');
757
+ assert.equal(persisted.Environment.KBs.sect80.Major, '8');
758
+ } finally {
759
+ removeTempPath(tempRoot, { recursive: true, force: true });
760
+ }
761
+ });
762
+
763
+ test('classic gxi identity does not invent a GX8/GX9 mismatch before provider open', () => {
764
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-kb-gxi-compare-'));
765
+ const gx8Root = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-gx8-compare-'));
766
+ try {
767
+ fs.writeFileSync(path.join(tempRoot, 'Legacy.gxi'), 'classic-kb');
768
+ fs.writeFileSync(path.join(gx8Root, 'gxw32.exe'), 'not-a-real-executable');
769
+ const compatibility = compareGeneXusKbAndInstallation(tempRoot, gx8Root, {
770
+ readExecutableVersion: (exePath) => exePath.endsWith('gxw32.exe') ? '8.0.0.632' : null
771
+ });
772
+ assert.equal(compatibility.status, 'unresolved');
773
+ assert.equal(compatibility.kb.source, 'gxi-classic');
774
+ assert.equal(compatibility.gx.major, '8');
775
+ } finally {
776
+ removeTempPath(tempRoot, { recursive: true, force: true });
777
+ removeTempPath(gx8Root, { recursive: true, force: true });
778
+ }
779
+ });
780
+
663
781
  test('init rejects a known KB and SDK major mismatch before writing config', () => {
664
782
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-mismatch-'));
665
783
  const kbDir = path.join(tempRoot, 'kb17');
@@ -1056,6 +1174,146 @@ test('--fields validation returns usage error for invalid doctor field', () => {
1056
1174
  assert.equal(parsed.error.code, 'usage_error');
1057
1175
  });
1058
1176
 
1177
+ test('doctor client_config_sync describes the effective gateway and its source', () => {
1178
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-doctor-gateway-source-'));
1179
+ try {
1180
+ const env = sandboxHomeEnv(tempRoot);
1181
+ const configuredGateway = path.join(tempRoot, 'checkout', 'publish', 'GxMcp.Gateway.exe');
1182
+ const mismatchedGateway = path.join(tempRoot, 'other', 'GxMcp.Gateway.exe');
1183
+ const clientConfig = path.join(tempRoot, '.gemini', 'settings.json');
1184
+ fs.mkdirSync(path.dirname(configuredGateway), { recursive: true });
1185
+ fs.mkdirSync(path.dirname(clientConfig), { recursive: true });
1186
+ fs.writeFileSync(configuredGateway, '');
1187
+ fs.writeFileSync(clientConfig, JSON.stringify({
1188
+ mcpServers: {
1189
+ genexus18mcp: { command: configuredGateway, args: [] }
1190
+ }
1191
+ }));
1192
+
1193
+ const overrideEnv = { ...env, GENEXUS_MCP_GATEWAY_EXE: configuredGateway };
1194
+ const matching = runCli(['doctor', '--format', 'json'], { env: overrideEnv });
1195
+ assert.equal(matching.status, 0);
1196
+ const matchingCheck = JSON.parse(matching.stdout).ok.checks.find((c) => c.id === 'client_config_sync');
1197
+ assert.ok(matchingCheck);
1198
+ assert.equal(matchingCheck.status, 'pass');
1199
+ assert.ok(matchingCheck.detail.includes(`Configured Gateway: ${configuredGateway} (source: GENEXUS_MCP_GATEWAY_EXE)`));
1200
+ assert.doesNotMatch(matchingCheck.detail, /npm-package gateway exe/);
1201
+
1202
+ fs.writeFileSync(clientConfig, JSON.stringify({
1203
+ mcpServers: {
1204
+ genexus18mcp: { command: mismatchedGateway, args: [] }
1205
+ }
1206
+ }));
1207
+ const mismatching = runCli(['doctor', '--format', 'json'], { env: overrideEnv });
1208
+ assert.equal(mismatching.status, 0);
1209
+ const mismatchingCheck = JSON.parse(mismatching.stdout).ok.checks.find((c) => c.id === 'client_config_sync');
1210
+ assert.ok(mismatchingCheck);
1211
+ assert.equal(mismatchingCheck.status, 'warn');
1212
+ assert.ok(mismatchingCheck.detail.includes(`Configured Gateway: ${configuredGateway} (source: GENEXUS_MCP_GATEWAY_EXE)`));
1213
+ assert.doesNotMatch(mismatchingCheck.detail, /this npm package's bundled exe/);
1214
+ } finally {
1215
+ removeTempPath(tempRoot, { recursive: true, force: true });
1216
+ }
1217
+ });
1218
+
1219
+ test('doctor fails closed when GX_CONFIG_PATH points at a missing file', () => {
1220
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-doctor-config-selection-'));
1221
+ try {
1222
+ const cwdConfig = path.join(tempRoot, 'config.json');
1223
+ const missingConfig = path.join(tempRoot, 'missing.json');
1224
+ fs.writeFileSync(cwdConfig, JSON.stringify({ Environment: { KBPath: 'C:\\wrong-context' } }));
1225
+
1226
+ const result = runCli(['doctor', '--mcp-smoke', '--format', 'json'], {
1227
+ cwd: tempRoot,
1228
+ env: {
1229
+ ...sandboxHomeEnv(tempRoot),
1230
+ GX_CONFIG_PATH: missingConfig,
1231
+ GENEXUS_MCP_GATEWAY_EXE: process.execPath
1232
+ }
1233
+ });
1234
+ assert.equal(result.status, 0);
1235
+ const checks = JSON.parse(result.stdout).ok.checks;
1236
+ const configCheck = checks.find((c) => c.id === 'config_file');
1237
+ const probeCheck = checks.find((c) => c.id === 'gateway_spawn_probe');
1238
+ const smokeCheck = checks.find((c) => c.id === 'mcp_smoke');
1239
+ assert.ok(configCheck);
1240
+ assert.equal(configCheck.status, 'fail');
1241
+ assert.ok(configCheck.detail.includes(missingConfig));
1242
+ assert.ok(probeCheck);
1243
+ assert.equal(probeCheck.status, 'not_applicable');
1244
+ assert.ok(smokeCheck);
1245
+ assert.equal(smokeCheck.status, 'not_applicable');
1246
+ } finally {
1247
+ removeTempPath(tempRoot, { recursive: true, force: true });
1248
+ }
1249
+ });
1250
+
1251
+ test('doctor rejects malformed tool definitions instead of reporting only file presence', () => {
1252
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-doctor-tool-defs-'));
1253
+ try {
1254
+ const configPath = path.join(tempRoot, 'config.json');
1255
+ const toolDefinitionsPath = path.join(tempRoot, 'tool_definitions.json');
1256
+ fs.writeFileSync(configPath, JSON.stringify({}));
1257
+ fs.writeFileSync(toolDefinitionsPath, '{ invalid json');
1258
+
1259
+ const result = runCli(['doctor', '--format', 'json'], {
1260
+ cwd: tempRoot,
1261
+ env: {
1262
+ ...sandboxHomeEnv(tempRoot),
1263
+ GX_CONFIG_PATH: configPath,
1264
+ GENEXUS_MCP_GATEWAY_EXE: path.join(tempRoot, 'missing', 'GxMcp.Gateway.exe'),
1265
+ GENEXUS_MCP_TOOL_DEFINITIONS: toolDefinitionsPath
1266
+ }
1267
+ });
1268
+ assert.equal(result.status, 0);
1269
+ const check = JSON.parse(result.stdout).ok.checks.find((c) => c.id === 'tool_definitions');
1270
+ assert.ok(check);
1271
+ assert.equal(check.status, 'fail');
1272
+ assert.ok(check.detail.includes(toolDefinitionsPath));
1273
+ } finally {
1274
+ removeTempPath(tempRoot, { recursive: true, force: true });
1275
+ }
1276
+ });
1277
+
1278
+ test('doctor reports strict KB catalog ambiguity instead of ignoring Environment.KBs', () => {
1279
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-doctor-kb-catalog-'));
1280
+ try {
1281
+ const kbOne = path.join(tempRoot, 'kb-one');
1282
+ const kbTwo = path.join(tempRoot, 'kb-two');
1283
+ const configPath = path.join(tempRoot, 'config.json');
1284
+ fs.mkdirSync(kbOne, { recursive: true });
1285
+ fs.mkdirSync(kbTwo, { recursive: true });
1286
+ fs.writeFileSync(path.join(kbOne, 'model.gxw'), '');
1287
+ fs.writeFileSync(path.join(kbTwo, 'model.gxw'), '');
1288
+ fs.writeFileSync(configPath, JSON.stringify({
1289
+ GatewayMode: 'stdio-isolated',
1290
+ Environment: {
1291
+ ResolutionPolicy: 'strict',
1292
+ KBs: {
1293
+ one: { Path: kbOne },
1294
+ two: { Path: kbTwo }
1295
+ }
1296
+ }
1297
+ }));
1298
+
1299
+ const result = runCli(['doctor', '--format', 'json'], {
1300
+ cwd: tempRoot,
1301
+ env: {
1302
+ ...sandboxHomeEnv(tempRoot),
1303
+ GX_CONFIG_PATH: configPath,
1304
+ GENEXUS_MCP_GATEWAY_EXE: path.join(tempRoot, 'missing', 'GxMcp.Gateway.exe')
1305
+ }
1306
+ });
1307
+ assert.equal(result.status, 0);
1308
+ const check = JSON.parse(result.stdout).ok.checks.find((c) => c.id === 'kb_catalog');
1309
+ assert.ok(check);
1310
+ assert.equal(check.status, 'warn');
1311
+ assert.match(check.detail, /neither ActiveKb nor DefaultKb/);
1312
+ } finally {
1313
+ removeTempPath(tempRoot, { recursive: true, force: true });
1314
+ }
1315
+ });
1316
+
1059
1317
  test('doctor finds tool_definitions.json next to the gateway exe (not just dev-tree)', () => {
1060
1318
  // Regression for v2.6.6 bug: getToolDefinitionsPath() hard-coded the dev-tree
1061
1319
  // location, so every installed copy reported "tool_definitions.json is missing"
@@ -63,15 +63,17 @@
63
63
  "driver": "com-gxpublic",
64
64
  "defaultInstallPath": "C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 9.0",
65
65
  "registryNames": ["GeneXus 9.0"],
66
- "legacyRegistryVersions": ["9.0"]
66
+ "legacyRegistryVersions": ["9.0"],
67
+ "legacySetupVersions": ["90"]
67
68
  },
68
69
  {
69
70
  "major": "8",
70
71
  "displayName": "GeneXus 8.0",
71
72
  "driver": "com-gxpublic",
72
- "defaultInstallPath": "C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 8.0",
73
+ "defaultInstallPath": "C:\\Program Files (x86)\\ARTech\\GeneXus\\gxw80",
73
74
  "registryNames": ["GeneXus 8.0"],
74
- "legacyRegistryVersions": ["8.0"]
75
+ "legacyRegistryVersions": ["8.0"],
76
+ "legacySetupVersions": ["80"]
75
77
  }
76
78
  ]
77
79
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "genexus-mcp",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "mcpName": "io.github.lennix1337/genexus",
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.",
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",
@@ -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.6.0": {
10
+ "GxMcp.Gateway/3.7.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.6.0": {
69
+ "GxMcp.Gateway/3.7.0": {
70
70
  "type": "project",
71
71
  "serviceable": false,
72
72
  "sha512": ""
Binary file
Binary file