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
@@ -152,30 +152,51 @@ function buildStatusData(cwd) {
152
152
  const configPath = resolveConfigPathNoMutate(cwd);
153
153
  const gatewayExePath = getGatewayExePath();
154
154
  const gatewayExeFound = fs.existsSync(gatewayExePath);
155
- const configFound = !!configPath;
155
+ const configFound = !!(configPath && fs.existsSync(configPath));
156
156
 
157
157
  let kbLooksValid = false;
158
158
  let kbPath = null;
159
+ let kbCatalog = { kbs: {}, activeKb: null, kbPath: null };
159
160
  let gxPath = null;
160
161
  let configSource = null;
162
+ let configReadable = false;
163
+ let configError = null;
161
164
 
162
- if (process.env.GX_CONFIG_PATH && fs.existsSync(process.env.GX_CONFIG_PATH)) {
165
+ if (process.env.GX_CONFIG_PATH) {
163
166
  configSource = 'env';
164
- } else if (configPath) {
167
+ } else if (configFound) {
165
168
  configSource = 'cwd';
166
169
  }
167
170
 
168
- if (configPath) {
171
+ if (configFound) {
169
172
  const cfg = readJsonFileSafe(configPath);
170
- if (cfg) {
171
- kbPath = cfg.Environment && cfg.Environment.KBPath ? cfg.Environment.KBPath : null;
172
- gxPath = cfg.GeneXus && cfg.GeneXus.InstallationPath ? cfg.GeneXus.InstallationPath : null;
173
+ if (cfg && typeof cfg === 'object' && !Array.isArray(cfg)) {
174
+ configReadable = true;
175
+ const environment = cfg.Environment && typeof cfg.Environment === 'object' ? cfg.Environment : {};
176
+ kbCatalog = readKbCatalog(configPath, cfg);
177
+ const declaredKbs = Object.entries(kbCatalog.kbs || {});
178
+ const catalogPath = kbCatalog.activeKb
179
+ ? kbCatalog.kbs[kbCatalog.activeKb] || null
180
+ : (declaredKbs.length === 1 ? declaredKbs[0][1] : null);
181
+ const legacyKbPath = typeof environment.KBPath === 'string' && environment.KBPath.trim()
182
+ ? environment.KBPath
183
+ : null;
184
+ kbPath = legacyKbPath || catalogPath || null;
185
+ gxPath = cfg.GeneXus && typeof cfg.GeneXus.InstallationPath === 'string'
186
+ ? cfg.GeneXus.InstallationPath
187
+ : null;
173
188
  if (kbPath) kbLooksValid = directoryLooksLikeKnowledgeBase(kbPath);
189
+ } else {
190
+ configError = 'Configuration file is not a readable JSON object.';
174
191
  }
192
+ } else if (configPath) {
193
+ configError = process.env.GX_CONFIG_PATH
194
+ ? 'GX_CONFIG_PATH points to a missing configuration file.'
195
+ : 'Configuration file does not exist.';
175
196
  }
176
197
 
177
198
  const ready = configFound && gatewayExeFound;
178
- return { ready, configFound, gatewayExeFound, kbLooksValid, configPath, gatewayExePath, kbPath, gxPath, configSource };
199
+ return { ready, configFound, configReadable, configError, gatewayExeFound, kbLooksValid, kbCatalog, configPath, gatewayExePath, kbPath, gxPath, configSource };
179
200
  }
180
201
 
181
202
  const PROBE_EXIT_GRACE_MS = 2000;
@@ -209,19 +230,116 @@ function stopProbeChild(child, graceMs = PROBE_EXIT_GRACE_MS) {
209
230
  });
210
231
  }
211
232
 
233
+ async function runGatewaySelfTest({ env = process.env, timeoutMs = 5000 }) {
234
+ const gatewayExePath = getGatewayExePath();
235
+ const maxOutputChars = 32 * 1024;
236
+
237
+ const appendOutput = (current, chunk) => {
238
+ const next = current + chunk.toString();
239
+ return next.length > maxOutputChars ? next.slice(-maxOutputChars) : next;
240
+ };
241
+
242
+ const summarize = (code, stdout, stderr) => {
243
+ let payload = null;
244
+ const lines = stdout.split('\n').map((line) => line.trim()).filter(Boolean);
245
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
246
+ try {
247
+ payload = JSON.parse(lines[i]);
248
+ break;
249
+ } catch {
250
+ // Keep looking: a diagnostic line may precede the JSON envelope.
251
+ }
252
+ }
253
+
254
+ if (payload && payload.schemaVersion === 'gateway-selftest/1' && Array.isArray(payload.checks)) {
255
+ const failed = payload.checks.find((check) => check.status === 'fail');
256
+ if (code === 0 && payload.ok !== false && !failed) {
257
+ return {
258
+ status: 'pass',
259
+ detail: `Gateway self-test passed (${payload.summary?.total || payload.checks.length} checks).`
260
+ };
261
+ }
262
+ const reason = failed && failed.detail
263
+ ? `: ${failed.id}: ${sanitizeOperationalMessage(failed.detail)}`
264
+ : ` (exit ${code === null ? 'unknown' : code})`;
265
+ return { status: 'fail', detail: `Gateway self-test failed${reason}.` };
266
+ }
267
+
268
+ const preview = sanitizeOperationalMessage((stderr || stdout).trim(), '');
269
+ return {
270
+ status: code === 0 ? 'warn' : 'fail',
271
+ detail: code === 0
272
+ ? (preview ? `Gateway self-test returned no recognized result: ${preview}` : 'Gateway self-test returned no recognized result.')
273
+ : (preview
274
+ ? `Gateway self-test failed (exit ${code === null ? 'unknown' : code}): ${preview}`
275
+ : `Gateway self-test failed (exit ${code === null ? 'unknown' : code}) with no diagnostic output.`)
276
+ };
277
+ };
278
+
279
+ return await new Promise((resolve) => {
280
+ let child;
281
+ let timer = null;
282
+ let settled = false;
283
+ let timedOut = false;
284
+ let stdout = '';
285
+ let stderr = '';
286
+ const finish = (result) => {
287
+ if (settled) return;
288
+ settled = true;
289
+ if (timer) clearTimeout(timer);
290
+ resolve(result);
291
+ };
292
+
293
+ try {
294
+ child = spawn(gatewayExePath, ['--self-test'], {
295
+ stdio: ['ignore', 'pipe', 'pipe'],
296
+ windowsHide: true,
297
+ env
298
+ });
299
+ child.stdout.on('data', (chunk) => { stdout = appendOutput(stdout, chunk); });
300
+ child.stderr.on('data', (chunk) => { stderr = appendOutput(stderr, chunk); });
301
+ child.once('error', (err) => {
302
+ finish({ status: 'fail', detail: `Gateway self-test could not start: ${sanitizeOperationalMessage(err.message)}` });
303
+ });
304
+ child.once('exit', (code) => {
305
+ if (timedOut) {
306
+ finish({ status: 'warn', detail: `Gateway self-test timed out after ${timeoutMs}ms.` });
307
+ return;
308
+ }
309
+ finish(summarize(code, stdout, stderr));
310
+ });
311
+ timer = setTimeout(async () => {
312
+ if (settled) return;
313
+ timedOut = true;
314
+ const exited = await stopProbeChild(child);
315
+ finish({
316
+ status: 'warn',
317
+ detail: exited
318
+ ? `Gateway self-test timed out after ${timeoutMs}ms; process was stopped.`
319
+ : `Gateway self-test timed out after ${timeoutMs}ms and did not exit after the stop signal.`
320
+ });
321
+ }, timeoutMs);
322
+ } catch (err) {
323
+ finish({ status: 'fail', detail: `Gateway self-test could not start: ${sanitizeOperationalMessage(err.message)}` });
324
+ }
325
+ });
326
+ }
327
+
212
328
  async function spawnGatewayProbe({ env = process.env, spawnHoldMs, timeoutMs, label, successDetail }) {
213
329
  const gatewayExePath = getGatewayExePath();
214
330
 
215
331
  return await new Promise((resolve) => {
216
332
  let done = false;
333
+ let timer = null;
217
334
  const finish = (result) => {
218
335
  if (done) return;
219
336
  done = true;
337
+ if (timer) clearTimeout(timer);
220
338
  resolve(result);
221
339
  };
222
340
 
223
341
  try {
224
- const child = spawn(gatewayExePath, ['--axi-spawn-probe'], {
342
+ const child = spawn(gatewayExePath, [], {
225
343
  stdio: 'ignore',
226
344
  windowsHide: true,
227
345
  env
@@ -241,6 +359,16 @@ async function spawnGatewayProbe({ env = process.env, spawnHoldMs, timeoutMs, la
241
359
  }
242
360
  });
243
361
 
362
+ child.once('exit', (code, signal) => {
363
+ if (done) return;
364
+ finish({
365
+ status: code === 0 ? 'warn' : 'fail',
366
+ detail: code === 0
367
+ ? `${label} exited before the observation window (signal: ${signal || 'none'}).`
368
+ : `${label} exited with code ${code === null ? 'unknown' : code}${signal ? ` (signal: ${signal})` : ''}.`
369
+ });
370
+ });
371
+
244
372
  child.once('spawn', () => {
245
373
  setTimeout(async () => {
246
374
  const exited = await stopProbeChild(child);
@@ -252,7 +380,7 @@ async function spawnGatewayProbe({ env = process.env, spawnHoldMs, timeoutMs, la
252
380
  }, spawnHoldMs);
253
381
  });
254
382
 
255
- setTimeout(async () => {
383
+ timer = setTimeout(async () => {
256
384
  if (done) return;
257
385
  const exited = await stopProbeChild(child);
258
386
  finish({
@@ -268,13 +396,11 @@ async function spawnGatewayProbe({ env = process.env, spawnHoldMs, timeoutMs, la
268
396
  });
269
397
  }
270
398
 
271
- async function probeGatewaySpawn() {
272
- return spawnGatewayProbe({
273
- spawnHoldMs: 180,
274
- timeoutMs: 900,
275
- label: 'Spawn probe',
276
- successDetail: 'Gateway process can be spawned (probe launched and terminated).'
277
- });
399
+ async function probeGatewaySpawn({ configPath = null } = {}) {
400
+ const env = configPath
401
+ ? { ...process.env, GX_CONFIG_PATH: configPath }
402
+ : process.env;
403
+ return runGatewaySelfTest({ env });
278
404
  }
279
405
 
280
406
  function resolveMcpSmokeTarget(cwd) {
@@ -284,9 +410,23 @@ function resolveMcpSmokeTarget(cwd) {
284
410
  return { applicable: true, status: null, detail: null, baseUrl: fallback };
285
411
  }
286
412
 
413
+ if (!fs.existsSync(configPath)) {
414
+ return {
415
+ applicable: false,
416
+ status: 'not_applicable',
417
+ detail: `MCP HTTP smoke skipped: resolved config file does not exist at ${configPath}.`,
418
+ baseUrl: null
419
+ };
420
+ }
421
+
287
422
  const cfg = readJsonFileSafe(configPath);
288
- if (!cfg || typeof cfg !== 'object') {
289
- return { applicable: true, status: null, detail: null, baseUrl: fallback };
423
+ if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) {
424
+ return {
425
+ applicable: false,
426
+ status: 'not_applicable',
427
+ detail: `MCP HTTP smoke skipped: resolved config file is not a readable JSON object at ${configPath}.`,
428
+ baseUrl: null
429
+ };
290
430
  }
291
431
 
292
432
  const server = cfg.Server && typeof cfg.Server === 'object' ? cfg.Server : {};
@@ -437,9 +577,15 @@ async function handleStatus(options, ctx) {
437
577
  };
438
578
  }
439
579
 
440
- function buildClientExeCrossCheck(packageExePath) {
580
+ function buildClientExeCrossCheck(packageExePath, clientRows = null) {
441
581
  const packageNorm = normalizeExePath(packageExePath);
442
- const targets = filterClientTargets(getClientConfigTargets(), { platform: process.platform });
582
+ const gatewaySource = process.env.GENEXUS_MCP_GATEWAY_EXE
583
+ ? 'GENEXUS_MCP_GATEWAY_EXE'
584
+ : 'packaged gateway (default)';
585
+ const gatewayTarget = `Configured Gateway: ${packageExePath} (source: ${gatewaySource})`;
586
+ const targets = clientRows
587
+ ? clientRows.map((row) => ({ name: row.name, path: row.configPath, entry: row.command ? { command: row.command } : null }))
588
+ : filterClientTargets(getClientConfigTargets(), { platform: process.platform });
443
589
 
444
590
  const mismatches = [];
445
591
  const matches = [];
@@ -447,13 +593,17 @@ function buildClientExeCrossCheck(packageExePath) {
447
593
  let inspected = 0;
448
594
 
449
595
  for (const client of targets) {
450
- if (!fs.existsSync(client.path)) continue;
451
596
  let entry;
452
- try {
453
- entry = readClientCommandEntry(client);
454
- } catch (err) {
455
- errors.push(`${client.name}: ${err.message || 'read failed'}`);
456
- continue;
597
+ if (clientRows) {
598
+ entry = client.entry;
599
+ } else {
600
+ if (!fs.existsSync(client.path)) continue;
601
+ try {
602
+ entry = readClientCommandEntry(client);
603
+ } catch (err) {
604
+ errors.push(`${client.name}: ${err.message || 'read failed'}`);
605
+ continue;
606
+ }
457
607
  }
458
608
  if (!entry || !entry.command) continue;
459
609
  inspected += 1;
@@ -489,14 +639,14 @@ function buildClientExeCrossCheck(packageExePath) {
489
639
  if (mismatches.length === 0) {
490
640
  return {
491
641
  status: 'pass',
492
- detail: `All inspected client configs (${matches.join(', ')}) point at the npm-package gateway exe.`
642
+ detail: `All inspected client configs (${matches.join(', ')}) match the configured gateway target. ${gatewayTarget}.`
493
643
  };
494
644
  }
495
645
 
496
646
  const detailParts = mismatches.map((m) => `${m.client} -> ${m.configured}${m.exists ? '' : ' (missing)'}`);
497
647
  return {
498
648
  status: 'warn',
499
- detail: `Client(s) reference a gateway exe that is NOT this npm package's bundled exe. \`npm install -g genexus-mcp@latest\` will NOT update those instances. Bundled: ${packageExePath}. Mismatches: ${detailParts.join('; ')}. Re-run scripts/install.ps1 (or genexus-mcp init --write-clients) to resync.`
649
+ detail: `Client(s) reference a gateway exe that is not the configured target. ${gatewayTarget}. \`npm install -g genexus-mcp@latest\` will NOT update those instances. Mismatches: ${detailParts.join('; ')}. Re-run scripts/install.ps1 (or genexus-mcp init --write-clients) to resync.`
500
650
  };
501
651
  }
502
652
 
@@ -603,25 +753,37 @@ function buildInProcessBuildAssemblyLoadCheck(gxPath) {
603
753
  };
604
754
  }
605
755
 
606
- function buildGxPublicComCheck(gxMajor, gxPath) {
756
+ function buildGxPublicComCheck(gxMajor) {
607
757
  if (gxMajor !== '8' && gxMajor !== '9') {
608
758
  return { id: 'gxpublic_com_registration', status: 'not_applicable', detail: 'Not a GeneXus 8.0/9.0 installation.' };
609
759
  }
610
760
  const child_process = require('child_process');
611
- let isRegistered = false;
612
- try {
613
- const res = child_process.spawnSync('reg', ['query', 'HKCR\\GXPublic.GXPublic'], { encoding: 'utf8', timeout: 2000 });
614
- isRegistered = res.status === 0;
615
- } catch { }
761
+ const providers = ['GXPublic.GXPublic.4', 'GXPublic.GXPublic.5', 'GXPubGXX.GXPublic.5', 'GXPubGXX.GXPublic'];
762
+ const registered = [];
763
+ for (const provider of providers) {
764
+ try {
765
+ const res = child_process.spawnSync('reg', ['query', `HKCR\\${provider}`], { encoding: 'utf8', timeout: 2000 });
766
+ if (res.status === 0) registered.push(provider);
767
+ } catch { }
768
+ }
616
769
 
617
- if (isRegistered) {
618
- return { id: 'gxpublic_com_registration', status: 'pass', detail: 'GXPublic COM automation server (GXPublic.GXPublic) is registered.' };
770
+ if (registered.length > 0) {
771
+ const matching = gxMajor === '8'
772
+ ? ['GXPublic.GXPublic.4', 'GXPubGXX.GXPublic.5', 'GXPubGXX.GXPublic']
773
+ : ['GXPublic.GXPublic.5', 'GXPubGXX.GXPublic.5', 'GXPubGXX.GXPublic'];
774
+ const hasMatchingProvider = registered.some((provider) => matching.includes(provider));
775
+ return {
776
+ id: 'gxpublic_com_registration',
777
+ status: hasMatchingProvider ? 'pass' : 'warn',
778
+ detail: hasMatchingProvider
779
+ ? `GXPublic OLE DB provider(s) registered for the 32-bit Worker: ${registered.join(', ')}.`
780
+ : `GXPublic provider(s) registered for the 32-bit Worker: ${registered.join(', ')}, but none is the documented GeneXus ${gxMajor}.0 provider (${matching.join(', ')}). Install the matching version before opening this KB.`
781
+ };
619
782
  }
620
- const dllHint = gxPath ? path.join(gxPath, 'GxPublic.dll') : 'GxPublic.dll';
621
783
  return {
622
784
  id: 'gxpublic_com_registration',
623
785
  status: 'warn',
624
- detail: `GXPublic COM component is not registered. Run 'regsvr32 "${dllHint}"' to enable GeneXus ${gxMajor}.0 COM automation.`
786
+ detail: `No supported GXPublic OLE DB provider is registered for GeneXus ${gxMajor}.0. Install the versioned GXPublic provider matching the KB generation (GXPublic 8.0 for GX8 or GXPublic Yi for GX9).`
625
787
  };
626
788
  }
627
789
 
@@ -669,7 +831,21 @@ function redactConfig(cfg) {
669
831
  return walk(cfg);
670
832
  }
671
833
 
672
- async function buildSupportDump({ checks, summary, data, gatewayExePath, ctx }) {
834
+ function redactDiagnosticText(value, paths) {
835
+ let text = String(value ?? '');
836
+ const candidates = [...new Set((paths || []).filter((candidate) => typeof candidate === 'string' && candidate.length > 0))]
837
+ .sort((a, b) => b.length - a.length);
838
+ for (const candidate of candidates) {
839
+ const token = `<redacted:${require('crypto').createHash('sha256').update(candidate).digest('hex').slice(0, 8)}>`;
840
+ for (const variant of new Set([candidate, candidate.replace(/\\/g, '/')])) {
841
+ const escaped = variant.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
842
+ text = text.replace(new RegExp(escaped, 'gi'), token);
843
+ }
844
+ }
845
+ return text;
846
+ }
847
+
848
+ async function buildSupportDump({ checks, summary, data, gatewayExePath, clientRows = [], toolDefPath, stdioErrorLogPath, ctx }) {
673
849
  const os = require('os');
674
850
  const crypto = require('crypto');
675
851
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
@@ -685,7 +861,23 @@ async function buildSupportDump({ checks, summary, data, gatewayExePath, ctx })
685
861
  entries.push(relPath);
686
862
  };
687
863
 
688
- writeEntry('doctor.json', { summary, checks, generatedAt: new Date().toISOString() });
864
+ const redactionPaths = [
865
+ gatewayExePath,
866
+ data.configPath,
867
+ data.kbPath,
868
+ data.gxPath,
869
+ toolDefPath,
870
+ stdioErrorLogPath,
871
+ ...Object.values(data.kbCatalog?.kbs || {}),
872
+ ...clientRows.flatMap((row) => [row.configPath, row.command])
873
+ ];
874
+ const redactedChecks = checks.map((check) => ({
875
+ ...check,
876
+ detail: redactDiagnosticText(check.detail, redactionPaths)
877
+ }));
878
+ const redactPath = (value) => redactDiagnosticText(value, redactionPaths);
879
+
880
+ writeEntry('doctor.json', { summary, checks: redactedChecks, generatedAt: new Date().toISOString() });
689
881
 
690
882
  if (data.configPath && fs.existsSync(data.configPath)) {
691
883
  const cfg = readJsonFileSafe(data.configPath);
@@ -701,7 +893,7 @@ async function buildSupportDump({ checks, summary, data, gatewayExePath, ctx })
701
893
  nodeVersion: process.version,
702
894
  osRelease: os.release(),
703
895
  cwdHash: crypto.createHash('sha256').update(ctx.cwd || '').digest('hex').slice(0, 8),
704
- gatewayExePath,
896
+ gatewayExePath: redactPath(gatewayExePath),
705
897
  gatewayExeExists: fs.existsSync(gatewayExePath),
706
898
  configSource: data.configSource,
707
899
  kbConfigured: !!data.kbPath,
@@ -783,27 +975,38 @@ async function handleDoctor(options, ctx) {
783
975
  const stdioErrorLogExists = !!(stdioErrorLogPath && fs.existsSync(stdioErrorLogPath));
784
976
 
785
977
  let toolCount = 0;
978
+ let toolDefinitionsValid = false;
979
+ let toolDefinitionsError = null;
786
980
  if (toolDefsExists) {
787
981
  try {
788
982
  const parsed = JSON.parse(fs.readFileSync(toolDefPath, 'utf8'));
789
- if (Array.isArray(parsed)) toolCount = parsed.length;
790
- } catch {
791
- toolCount = 0;
983
+ if (Array.isArray(parsed)) {
984
+ toolDefinitionsValid = true;
985
+ toolCount = parsed.length;
986
+ } else {
987
+ toolDefinitionsError = 'tool_definitions.json is not an array.';
988
+ }
989
+ } catch (err) {
990
+ toolDefinitionsError = sanitizeOperationalMessage(err && err.message ? err.message : 'invalid JSON');
792
991
  }
793
992
  }
794
993
 
795
994
  const kbPath = data.kbPath;
796
995
  const gxPath = data.gxPath;
797
996
  const kbExists = !!(kbPath && fs.existsSync(kbPath));
798
- const gxExeExists = !!(gxPath && (fs.existsSync(path.join(gxPath, 'genexus.exe')) || fs.existsSync(path.join(gxPath, 'gx.exe'))));
997
+ const gxExecutable = gxPath && ['genexus.exe', 'gx.exe', 'gxw32.exe']
998
+ .find((name) => fs.existsSync(path.join(gxPath, name)));
999
+ const gxExeExists = !!gxExecutable;
799
1000
  const kbSdkCompatibility = kbExists && gxExeExists
800
1001
  ? compareGeneXusKbAndInstallation(kbPath, gxPath)
801
1002
  : null;
802
1003
  const catalog = getGeneXusVersionCatalog();
803
1004
 
804
1005
  let unsupportedCompatibilityMajor = null;
805
- let compatibilityStatus = 'warn';
806
- let compatibilityDetail = 'KB/SDK major compatibility check was skipped until both configured paths exist.';
1006
+ let compatibilityStatus = kbPath && gxPath ? 'warn' : 'not_applicable';
1007
+ let compatibilityDetail = kbPath && gxPath
1008
+ ? 'KB/SDK major compatibility check was skipped until both configured paths exist.'
1009
+ : 'KB/SDK major compatibility check is not applicable until both paths are configured.';
807
1010
  if (kbSdkCompatibility) {
808
1011
  if (!isSupportedCatalogMajor(catalog, kbSdkCompatibility.kb.major)) {
809
1012
  unsupportedCompatibilityMajor = `KB major ${kbSdkCompatibility.kb.major}`;
@@ -829,10 +1032,48 @@ async function handleDoctor(options, ctx) {
829
1032
  const gxVersionLabel = gxIdent && gxIdent.version ? ` (GeneXus ${gxIdent.major || ''} version ${gxIdent.version})` : (gxIdent && gxIdent.major ? ` (GeneXus ${gxIdent.major})` : '');
830
1033
 
831
1034
  const riskyZone = isPathLikelyAppLockerBlocked(gatewayExePath);
832
- const clientCrossCheck = buildClientExeCrossCheck(gatewayExePath);
1035
+ const clientRows = clientsStatus();
1036
+ const clientCrossCheck = buildClientExeCrossCheck(gatewayExePath, clientRows);
1037
+ const configCheck = data.configFound
1038
+ ? (data.configReadable
1039
+ ? { status: 'pass', detail: `GX config file was found at ${data.configPath} (source: ${data.configSource}).` }
1040
+ : { status: 'fail', detail: `GX config file could not be parsed as a JSON object: ${data.configPath}.` })
1041
+ : {
1042
+ status: 'fail',
1043
+ detail: data.configPath
1044
+ ? `${data.configError || 'GX config file is missing.'} ${data.configPath}`
1045
+ : 'GX config file is missing.'
1046
+ };
1047
+ const toolDefinitionsCheck = !toolDefsExists
1048
+ ? {
1049
+ status: 'warn',
1050
+ detail: process.env.GENEXUS_MCP_TOOL_DEFINITIONS
1051
+ ? `tool_definitions.json missing at GENEXUS_MCP_TOOL_DEFINITIONS=${toolDefPath}. Unset the env var or point it at a valid file.`
1052
+ : `tool_definitions.json missing. Expected at ${toolDefPath} (next to the gateway exe). The csproj should copy it on publish — reinstall via scripts/install.ps1, or set GENEXUS_MCP_TOOL_DEFINITIONS to override.`
1053
+ }
1054
+ : toolDefinitionsValid
1055
+ ? { status: 'pass', detail: `Tool definition file found (${toolCount} tools) at ${toolDefPath}.` }
1056
+ : { status: 'fail', detail: `tool_definitions.json is invalid at ${toolDefPath}${toolDefinitionsError ? `: ${toolDefinitionsError}` : '.'}` };
1057
+ const gxEnvCheck = process.env.GX_CONFIG_PATH
1058
+ ? { status: 'pass', detail: 'GX_CONFIG_PATH env var is set.' }
1059
+ : data.configFound
1060
+ ? { status: 'not_applicable', detail: `GX_CONFIG_PATH is not set; using ${data.configPath} from the current directory.` }
1061
+ : { status: 'warn', detail: 'GX_CONFIG_PATH env var is not set and no config file was found.' };
1062
+ const kbCatalogEntries = Object.entries(data.kbCatalog?.kbs || {});
1063
+ const missingCatalogKbs = kbCatalogEntries.filter(([, declaredPath]) => !fs.existsSync(declaredPath));
1064
+ const selectedCatalogAlias = data.kbCatalog?.activeKb || (kbCatalogEntries.length === 1 ? kbCatalogEntries[0][0] : null);
1065
+ const kbCatalogCheck = kbCatalogEntries.length === 0
1066
+ ? { status: 'not_applicable', detail: 'No KB aliases are declared in Environment.KBs.' }
1067
+ : missingCatalogKbs.length > 0
1068
+ ? { status: 'fail', detail: `Declared KB path(s) missing: ${missingCatalogKbs.map(([alias, declaredPath]) => `${alias} -> ${declaredPath}`).join('; ')}.` }
1069
+ : data.kbCatalog.activeKb && !data.kbCatalog.kbs[data.kbCatalog.activeKb]
1070
+ ? { status: 'fail', detail: `Active/Default KB alias '${data.kbCatalog.activeKb}' is not declared in Environment.KBs.` }
1071
+ : kbCatalogEntries.length > 1 && !data.kbCatalog.activeKb
1072
+ ? { status: 'warn', detail: `${kbCatalogEntries.length} KB aliases are declared but neither ActiveKb nor DefaultKb selects one. Strict sessions must use an explicit kb.` }
1073
+ : { status: 'pass', detail: `${kbCatalogEntries.length} declared KB alias(es) are present${selectedCatalogAlias ? `; diagnostic target: ${selectedCatalogAlias}` : ''}.` };
833
1074
 
834
1075
  const checks = [
835
- { id: 'config_file', status: data.configFound ? 'pass' : 'fail', detail: data.configFound ? 'GX config file was found.' : 'GX config file is missing.' },
1076
+ { id: 'config_file', status: configCheck.status, detail: configCheck.detail },
836
1077
  { id: 'gateway_exe', status: data.gatewayExeFound ? 'pass' : 'fail', detail: data.gatewayExeFound ? 'Gateway executable is available.' : 'Gateway executable is missing.' },
837
1078
  {
838
1079
  id: 'stdio_error_log',
@@ -851,18 +1092,19 @@ async function handleDoctor(options, ctx) {
851
1092
  // A KB path configured but absent on disk is fatal — the worker can't open a KB
852
1093
  // that doesn't exist. Only when no KB is configured at all do we soften to warn.
853
1094
  { id: 'kb_path_exists', status: kbExists ? 'pass' : (kbPath ? 'fail' : 'warn'), detail: kbExists ? 'Configured KB path exists.' : (kbPath ? `Configured KB path does not exist: ${kbPath}` : 'No KB path is configured.') },
1095
+ { id: 'kb_catalog', status: kbCatalogCheck.status, detail: kbCatalogCheck.detail },
854
1096
  { id: 'kb_shape', status: data.kbLooksValid ? 'pass' : 'warn', detail: data.kbLooksValid ? 'KB folder shape looks valid.' : 'KB markers were not found in configured KB path.' },
855
- // Same logic for the GeneXus install: missing genexus.exe at a configured path
1097
+ // Same logic for the GeneXus install: missing the configured executable at a path
856
1098
  // guarantees a worker crash on first MCP call. Promote from warn to fail so init
857
1099
  // exits non-zero and the caller (install.ps1, AI client) actually sees the problem.
858
- { id: 'gx_installation', status: gxExeExists ? 'pass' : (gxPath ? 'fail' : 'warn'), detail: gxExeExists ? `GeneXus installation has ${gxPath && fs.existsSync(path.join(gxPath, 'gx.exe')) && !fs.existsSync(path.join(gxPath, 'genexus.exe')) ? 'gx.exe' : 'genexus.exe'}${gxVersionLabel}.` : (gxPath ? `Configured GeneXus installation is missing executable at: ${gxPath}` : 'No GeneXus installation path is configured.') },
1100
+ { id: 'gx_installation', status: gxExeExists ? 'pass' : (gxPath ? 'fail' : 'warn'), detail: gxExeExists ? `GeneXus installation has ${gxExecutable}${gxVersionLabel}.` : (gxPath ? `Configured GeneXus installation is missing executable at: ${gxPath}` : 'No GeneXus installation path is configured.') },
859
1101
  {
860
1102
  id: 'kb_sdk_compatibility',
861
1103
  status: compatibilityStatus,
862
1104
  detail: compatibilityDetail
863
1105
  },
864
- { id: 'tool_definitions', status: toolDefsExists ? 'pass' : 'warn', detail: toolDefsExists ? `Tool definition file found (${toolCount} tools) at ${toolDefPath}.` : (process.env.GENEXUS_MCP_TOOL_DEFINITIONS ? `tool_definitions.json missing at GENEXUS_MCP_TOOL_DEFINITIONS=${toolDefPath}. Unset the env var or point it at a valid file.` : `tool_definitions.json missing. Expected at ${toolDefPath} (next to the gateway exe). The csproj should copy it on publish — reinstall via scripts/install.ps1, or set GENEXUS_MCP_TOOL_DEFINITIONS to override.`) },
865
- { id: 'gx_env', status: process.env.GX_CONFIG_PATH ? 'pass' : 'warn', detail: process.env.GX_CONFIG_PATH ? 'GX_CONFIG_PATH env var is set.' : 'GX_CONFIG_PATH env var is not set for this process.' },
1106
+ { id: 'tool_definitions', status: toolDefinitionsCheck.status, detail: toolDefinitionsCheck.detail },
1107
+ { id: 'gx_env', status: gxEnvCheck.status, detail: gxEnvCheck.detail },
866
1108
  { id: 'client_config_sync', status: clientCrossCheck.status, detail: clientCrossCheck.detail }
867
1109
  ];
868
1110
 
@@ -880,7 +1122,6 @@ async function handleDoctor(options, ctx) {
880
1122
  checks.push({ id: 'legacy_ide_lock', status: ideLockCheck.status, detail: ideLockCheck.detail });
881
1123
 
882
1124
  // Client registration summary — one line answering "are my AI agents wired up?".
883
- const clientRows = clientsStatus();
884
1125
  const installedRows = clientRows.filter((r) => r.installed);
885
1126
  const staleRows = clientRows.filter((r) => r.commandStale);
886
1127
  const installedUnregistered = installedRows.filter((r) => !r.registered && r.writeSupported);
@@ -898,11 +1139,13 @@ async function handleDoctor(options, ctx) {
898
1139
  }
899
1140
  checks.push({ id: 'clients_registered', status: clientsStatusLevel, detail: clientsDetail });
900
1141
 
901
- if (data.gatewayExeFound) {
902
- const probe = await probeGatewaySpawn();
1142
+ if (data.gatewayExeFound && data.configFound && data.configReadable) {
1143
+ const probe = await probeGatewaySpawn({ configPath: data.configPath });
903
1144
  checks.push({ id: 'gateway_spawn_probe', status: probe.status, detail: probe.detail });
904
- } else {
1145
+ } else if (!data.gatewayExeFound) {
905
1146
  checks.push({ id: 'gateway_spawn_probe', status: 'warn', detail: 'Spawn probe skipped: gateway exe not found.' });
1147
+ } else {
1148
+ checks.push({ id: 'gateway_spawn_probe', status: 'not_applicable', detail: 'Gateway self-test skipped: no usable configuration was resolved.' });
906
1149
  }
907
1150
 
908
1151
  if (options.mcpSmoke) {
@@ -921,7 +1164,7 @@ async function handleDoctor(options, ctx) {
921
1164
  // to send 5 separate things over chat.
922
1165
  if (options.dump) {
923
1166
  try {
924
- const dumpResult = await buildSupportDump({ checks, summary, data, gatewayExePath, ctx });
1167
+ const dumpResult = await buildSupportDump({ checks, summary, data, gatewayExePath, clientRows, toolDefPath, stdioErrorLogPath, ctx });
925
1168
  return {
926
1169
  exitCode: ctx.EXIT_CODES.OK,
927
1170
  envelope: {
@@ -932,7 +1175,7 @@ async function handleDoctor(options, ctx) {
932
1175
  entries: dumpResult.entries,
933
1176
  summary
934
1177
  },
935
- help: ['Attach the zip to your support ticket. Paths inside config.json have been redacted; sensitive values may still appear in worker logs — review before sharing if needed.']
1178
+ help: ['Attach the zip to your support ticket. Paths in config.json and doctor metadata have been redacted; sensitive values may still appear in worker logs — review before sharing if needed.']
936
1179
  }
937
1180
  };
938
1181
  } catch (err) {
@@ -1506,7 +1749,9 @@ async function runInteractiveInit(ctx) {
1506
1749
  };
1507
1750
  }
1508
1751
 
1509
- if (!fs.existsSync(path.join(finalGx, 'genexus.exe'))) {
1752
+ const gxExecutable = finalGx && ['genexus.exe', 'gx.exe', 'gxw32.exe']
1753
+ .find((name) => fs.existsSync(path.join(finalGx, name)));
1754
+ if (!gxExecutable) {
1510
1755
  const suggested = discoverGeneXusInstallation(kbIdentity.major);
1511
1756
  const help = [`Path checked: ${finalGx}`];
1512
1757
  if (suggested && suggested.toLowerCase() !== finalGx.toLowerCase()) {
@@ -1516,7 +1761,7 @@ async function runInteractiveInit(ctx) {
1516
1761
  return {
1517
1762
  exitCode: ctx.EXIT_CODES.ERROR,
1518
1763
  envelope: operationalErrorEnvelope(
1519
- `GeneXus path does not contain genexus.exe. Aborted before writing config to avoid silent worker crashes.`,
1764
+ `GeneXus path does not contain a supported executable (genexus.exe, gx.exe, or gxw32.exe). Aborted before writing config to avoid silent worker crashes.`,
1520
1765
  ctx.EXIT_CODES.ERROR,
1521
1766
  help
1522
1767
  )
@@ -1725,7 +1970,9 @@ async function handleInit(options, ctx) {
1725
1970
  // install (e.g. C:\...\GeneXus18u7 vs the GeneXus18 default) used to slip through
1726
1971
  // — init wrote the config, then the worker crashed on first MCP call with no
1727
1972
  // useful signal back to the operator.
1728
- if (!fs.existsSync(path.join(resolution.gx.value, 'genexus.exe'))) {
1973
+ const gxExecutable = resolution.gx.value && ['genexus.exe', 'gx.exe', 'gxw32.exe']
1974
+ .find((name) => fs.existsSync(path.join(resolution.gx.value, name)));
1975
+ if (!gxExecutable) {
1729
1976
  const help = [
1730
1977
  `Path checked: ${resolution.gx.value}`,
1731
1978
  `Source: --${resolution.gx.source === 'flag' ? 'gx flag' : resolution.gx.source}`
@@ -1742,7 +1989,7 @@ async function handleInit(options, ctx) {
1742
1989
  return {
1743
1990
  exitCode: ctx.EXIT_CODES.ERROR,
1744
1991
  envelope: operationalErrorEnvelope(
1745
- `Configured GeneXus path does not contain genexus.exe. Init aborted before writing config to avoid silent worker crashes.`,
1992
+ `Configured GeneXus path does not contain a supported executable (genexus.exe, gx.exe, or gxw32.exe). Init aborted before writing config to avoid silent worker crashes.`,
1746
1993
  ctx.EXIT_CODES.ERROR,
1747
1994
  help
1748
1995
  )