genexus-mcp 3.6.1 → 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 +108 -9
  2. package/cli/commands/axi.js +311 -64
  3. package/cli/lib/config.js +99 -19
  4. package/cli/run.test.js +226 -0
  5. package/config/gx-versions.json +5 -3
  6. package/package.json +1 -1
  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 +12 -12
  12. package/publish/gxmcp-manifest.json +10 -10
  13. package/publish/gxmcp-sbom.json +4 -4
  14. package/publish/tool_definitions.json +4 -4
  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,14 +112,14 @@ 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
 
116
121
  function hasGeneXusExecutable(gxPath) {
117
- return Boolean(gxPath) && ['genexus.exe', 'gx.exe']
122
+ return Boolean(gxPath) && ['genexus.exe', 'gx.exe', 'gxw32.exe']
118
123
  .some((name) => fs.existsSync(path.join(gxPath, name)));
119
124
  }
120
125
 
@@ -165,6 +170,24 @@ function discoverGeneXusFromRegistry(preferredMajor = null) {
165
170
  } catch {
166
171
  }
167
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
+ }
168
191
  for (const legacyVersion of (entry.legacyRegistryVersions || [])) {
169
192
  const key = `${hive}\\GeneXus\\${legacyVersion}`;
170
193
  try {
@@ -259,7 +282,7 @@ function discoverGeneXusFromPath(preferredMajor = null) {
259
282
  if (process.platform !== 'win32') return null;
260
283
  try {
261
284
  const { execFileSync } = require('child_process');
262
- for (const executable of ['genexus.exe', 'gx.exe']) {
285
+ for (const executable of ['genexus.exe', 'gx.exe', 'gxw32.exe']) {
263
286
  let out;
264
287
  try {
265
288
  out = execFileSync('where.exe', [executable], {
@@ -361,7 +384,9 @@ function discoverKnowledgeBases(cwd, { maxResults = 25, scanDepth = 2 } = {}) {
361
384
  function directoryLooksLikeKnowledgeBase(dir) {
362
385
  try {
363
386
  const files = fs.readdirSync(dir);
364
- 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;
365
390
  } catch {
366
391
  return false;
367
392
  }
@@ -603,8 +628,11 @@ function migrateLegacyConfig(sourcePath, targetPath, { rejectNonMigratable = fal
603
628
 
604
629
  function resolveConfigPathNoMutate(cwd) {
605
630
  const cwdConfigPath = path.join(cwd, 'config.json');
606
- if (process.env.GX_CONFIG_PATH && fs.existsSync(process.env.GX_CONFIG_PATH)) {
607
- 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);
608
636
  }
609
637
  if (fs.existsSync(cwdConfigPath)) {
610
638
  return cwdConfigPath;
@@ -1927,6 +1955,9 @@ function readGeneXusInstallationIdentity(gxPath, options = {}) {
1927
1955
  if (!executableVersion) {
1928
1956
  executableVersion = readExecutableVersion(path.join(gxPath, 'gx.exe'));
1929
1957
  }
1958
+ if (!executableVersion) {
1959
+ executableVersion = readExecutableVersion(path.join(gxPath, 'gxw32.exe'));
1960
+ }
1930
1961
  if (executableVersion) {
1931
1962
  return {
1932
1963
  version: executableVersion,
@@ -2013,6 +2044,7 @@ function readGeneXusKbIdentity(kbPath) {
2013
2044
 
2014
2045
  let gxwFiles;
2015
2046
  let gxiFiles = [];
2047
+ let classicMarkerCount = 0;
2016
2048
  try {
2017
2049
  const allFiles = fs.readdirSync(kbPath);
2018
2050
  gxwFiles = allFiles
@@ -2021,6 +2053,8 @@ function readGeneXusKbIdentity(kbPath) {
2021
2053
  gxiFiles = allFiles
2022
2054
  .filter((fileName) => fileName.toLowerCase().endsWith('.gxi'))
2023
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;
2024
2058
  } catch {
2025
2059
  return { version: null, major: null, source: 'unavailable', reason: 'unreadable-kb-path' };
2026
2060
  }
@@ -2028,6 +2062,9 @@ function readGeneXusKbIdentity(kbPath) {
2028
2062
  if (gxiFiles.length > 0) {
2029
2063
  return { version: '9.0', major: '9', source: 'gxi-classic', reason: null };
2030
2064
  }
2065
+ if (classicMarkerCount >= 2) {
2066
+ return { version: null, major: null, source: 'classic-dat', reason: 'classic-generation-requires-provider' };
2067
+ }
2031
2068
  return { version: null, major: null, source: 'unavailable', reason: 'no-gxw' };
2032
2069
  }
2033
2070
  if (gxwFiles.length > 1) return { version: null, major: null, source: 'unavailable', reason: 'multiple-gxw' };
@@ -2049,34 +2086,65 @@ function readGeneXusKbIdentity(kbPath) {
2049
2086
  }
2050
2087
  }
2051
2088
 
2052
- function compareGeneXusKbAndInstallation(kbPath, gxPath) {
2089
+ function compareGeneXusKbAndInstallation(kbPath, gxPath, options = {}) {
2053
2090
  const kb = readGeneXusKbIdentity(kbPath);
2054
- const gx = readGeneXusInstallationIdentity(gxPath);
2091
+ const gx = readGeneXusInstallationIdentity(gxPath, options);
2055
2092
  let status = 'unresolved';
2056
- 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
+ }
2057
2101
  return { status, kb, gx };
2058
2102
  }
2059
2103
 
2060
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
+
2061
2128
  if (Array.isArray(raw)) {
2062
- const normalized = {};
2063
2129
  for (const entry of raw) {
2064
2130
  if (!entry || typeof entry !== 'object') continue;
2065
2131
  const name = typeof entry.alias === 'string' ? entry.alias : entry.Alias;
2066
- const kbPath = typeof entry.path === 'string' ? entry.path : entry.Path;
2067
- if (typeof name === 'string' && name && typeof kbPath === 'string' && kbPath) {
2068
- normalized[name] = kbPath;
2069
- }
2132
+ add(name, entry);
2070
2133
  }
2134
+ Object.defineProperty(normalized, '__entries', { value: entries, enumerable: false, writable: true });
2071
2135
  return normalized;
2072
2136
  }
2073
2137
 
2074
- 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;
2075
2143
  }
2076
2144
 
2077
- function readKbCatalog(configPath) {
2078
- if (!configPath) return { kbs: {}, activeKb: null, kbPath: null };
2079
- 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;
2080
2148
  if (!cfg) return { kbs: {}, activeKb: null, kbPath: null };
2081
2149
  const env = cfg.Environment || {};
2082
2150
  const activeKb = typeof env.ActiveKb === 'string' && env.ActiveKb
@@ -2092,7 +2160,19 @@ function readKbCatalog(configPath) {
2092
2160
  function writeKbCatalog(configPath, { kbs, activeKb, kbPath }) {
2093
2161
  const cfg = readJsonFileSafe(configPath) || {};
2094
2162
  cfg.Environment = cfg.Environment || {};
2095
- 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;
2096
2176
  if (activeKb) {
2097
2177
  cfg.Environment.ActiveKb = activeKb;
2098
2178
  cfg.Environment.DefaultKb = activeKb;
package/cli/run.test.js CHANGED
@@ -22,8 +22,11 @@ const {
22
22
  getGeneXusCatalogEntries,
23
23
  discoverGeneXusInstallation,
24
24
  readGeneXusInstallationIdentity,
25
+ directoryLooksLikeKnowledgeBase,
25
26
  readGeneXusKbIdentity,
26
27
  compareGeneXusKbAndInstallation,
28
+ readKbCatalog,
29
+ switchActiveKb,
27
30
  patchClientConfig
28
31
  } = require('./lib/config');
29
32
  const { handleInit, resolveMcpSmokeTarget } = require('./commands/axi');
@@ -571,6 +574,23 @@ test('classic GeneXus installation identity reads gx.exe metadata', () => {
571
574
  }
572
575
  });
573
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
+
574
594
  test('classic GeneXus installation discovery accepts GENEXUS_HOME with gx.exe', () => {
575
595
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-classic-home-'));
576
596
  const previousHome = process.env.GENEXUS_HOME;
@@ -692,6 +712,72 @@ test('KB identity reads GeneXus 9.0 classic major from gxi when gxw is absent',
692
712
  }
693
713
  });
694
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
+
695
781
  test('init rejects a known KB and SDK major mismatch before writing config', () => {
696
782
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-mismatch-'));
697
783
  const kbDir = path.join(tempRoot, 'kb17');
@@ -1088,6 +1174,146 @@ test('--fields validation returns usage error for invalid doctor field', () => {
1088
1174
  assert.equal(parsed.error.code, 'usage_error');
1089
1175
  });
1090
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
+
1091
1317
  test('doctor finds tool_definitions.json next to the gateway exe (not just dev-tree)', () => {
1092
1318
  // Regression for v2.6.6 bug: getToolDefinitionsPath() hard-coded the dev-tree
1093
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,6 +1,6 @@
1
1
  {
2
2
  "name": "genexus-mcp",
3
- "version": "3.6.1",
3
+ "version": "3.7.0",
4
4
  "mcpName": "io.github.lennix1337/genexus",
5
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 +7,7 @@
7
7
  "targets": {
8
8
  ".NETCoreApp,Version=v10.0": {},
9
9
  ".NETCoreApp,Version=v10.0/win-x64": {
10
- "GxMcp.Gateway/3.6.1": {
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.1": {
69
+ "GxMcp.Gateway/3.7.0": {
70
70
  "type": "project",
71
71
  "serviceable": false,
72
72
  "sha512": ""
Binary file
Binary file
@@ -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
  }
@@ -1,19 +1,19 @@
1
1
  {
2
- "Environment": {
3
- "ResolutionPolicy": "strict"
2
+ "GatewayMode": "stdio-isolated",
3
+ "ConfigSchemaVersion": 2,
4
+ "GeneXus": {
5
+ "WorkerExecutable": "worker\\\\GxMcp.Worker.exe",
6
+ "InstallationPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus18"
4
7
  },
5
8
  "Server": {
6
- "McpStdio": true,
7
- "TerseResponses": true,
8
- "HttpPort": 0,
9
9
  "WorkerIdleTimeoutMinutes": 5,
10
+ "HttpPort": 0,
11
+ "TerseResponses": true,
10
12
  "SessionIdleTimeoutMinutes": 10,
11
- "EmitStructuredContent": false
12
- },
13
- "ConfigSchemaVersion": 2,
14
- "GeneXus": {
15
- "InstallationPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus18",
16
- "WorkerExecutable": "worker\\\\GxMcp.Worker.exe"
13
+ "EmitStructuredContent": false,
14
+ "McpStdio": true
17
15
  },
18
- "GatewayMode": "stdio-isolated"
16
+ "Environment": {
17
+ "ResolutionPolicy": "strict"
18
+ }
19
19
  }
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": "gxmcp-release-manifest/1",
3
- "version": "3.6.1",
4
- "sourceCommit": "994b57d6eea67e439edda731572152bf13a41508",
3
+ "version": "3.7.0",
4
+ "sourceCommit": "1c6d0fb48c98ce2e88b5914a8b6f11510a0c9691",
5
5
  "sourceCommitPolicy": "exact-tag",
6
- "generatedAtUtc": "2026-09-18T14:27:29.5142024Z",
6
+ "generatedAtUtc": "2026-09-19T05:58:25.2316950Z",
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": "c9bfe92403a70dd3760b7685bc357491ec35c46c9b5a362dd9717017a3784532",
18
+ "schemaSha256": "de5ac0b8955a64065f4d9468b6b4e66610f476e43c9732c1c233b49feee5ddbb",
19
19
  "provenance": "gxmcp-sbom.json",
20
20
  "artifacts": [
21
21
  {
22
22
  "path": "gxmcp-sbom.json",
23
23
  "size": 590,
24
- "sha256": "3a437e76cc53aa499706dfd5da80e9ff8bc9d4b0176e47dc5bb1bb29112bdbdd"
24
+ "sha256": "7d32db9bbf7bab298561c8e5fe9f2e2518e4ac716daa371aa4dbd752ce61b300"
25
25
  },
26
26
  {
27
27
  "path": "GxMcp.Gateway.exe",
28
28
  "size": 162304,
29
- "sha256": "15765fd9430a58eb0ecb8f95ed3a3638be76fdb5c224103aefa0a44398945c61"
29
+ "sha256": "3507a7154b251a53deaadbb524298b1958ef017f4d59fc3ac369f04652ec9ac2"
30
30
  },
31
31
  {
32
32
  "path": "tool_definitions.json",
33
- "size": 125411,
34
- "sha256": "c9bfe92403a70dd3760b7685bc357491ec35c46c9b5a362dd9717017a3784532"
33
+ "size": 126023,
34
+ "sha256": "de5ac0b8955a64065f4d9468b6b4e66610f476e43c9732c1c233b49feee5ddbb"
35
35
  },
36
36
  {
37
37
  "path": "worker/GxMcp.Worker.exe",
38
- "size": 3342336,
39
- "sha256": "f837b7ce71044eaba8b84d9f7ccc258a15e9a75ba9be46b17c6c08670dffdfd6"
38
+ "size": 3414016,
39
+ "sha256": "9c1bb1f040661936e367706f353e5da14d0cc2e3ac0217b3b0ae1b5919fc5ce6"
40
40
  }
41
41
  ]
42
42
  }