genexus-mcp 3.8.0 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -813,6 +813,70 @@ function buildLegacyIdeLockCheck(gxMajor) {
813
813
  return { id: 'legacy_ide_lock', status: 'pass', detail: 'No conflicting GeneXus IDE process (gx.exe) detected.' };
814
814
  }
815
815
 
816
+ function buildRuntimeStagingCheck() {
817
+ try {
818
+ const {
819
+ resolveDefaultRuntimeRoot,
820
+ listStagedRuntimes,
821
+ probeRunningGxMcpProcesses,
822
+ getRuntimeProcessUsage,
823
+ classifyProcessRuntime,
824
+ shouldStage
825
+ } = require('../lib/runtime-stager');
826
+
827
+ const runtimeRoot = resolveDefaultRuntimeRoot();
828
+ const stagedList = listStagedRuntimes(runtimeRoot);
829
+ const processProbe = probeRunningGxMcpProcesses();
830
+ const runningProcs = processProbe.processes || [];
831
+ if (processProbe.ok === false) {
832
+ return {
833
+ id: 'runtime_staging',
834
+ status: 'warn',
835
+ detail: `Runtime process inspection was incomplete; GC is fail-closed. ${processProbe.error || 'The process probe did not return executable paths.'}`
836
+ };
837
+ }
838
+ const npxProcs = runningProcs.filter((p) => classifyProcessRuntime(p.exePath, runtimeRoot) === 'npx-cache');
839
+ const stagedProcs = runningProcs.filter((p) => classifyProcessRuntime(p.exePath, runtimeRoot) === 'staged');
840
+ const inUseRuntimes = getRuntimeProcessUsage(runtimeRoot, runningProcs);
841
+ const inUseDetail = inUseRuntimes.length > 0
842
+ ? ` Retained in-use runtime(s): ${inUseRuntimes.map((entry) => `${entry.name} (PIDs: ${entry.pids.join(', ')})`).join('; ')}.`
843
+ : '';
844
+
845
+ if (npxProcs.length > 0) {
846
+ return {
847
+ id: 'runtime_staging',
848
+ status: 'warn',
849
+ detail: `${npxProcs.length} process(es) running directly from npx cache (PIDs: ${npxProcs.map((p) => p.pid).join(', ')}). Terminate them to prevent EBUSY during upgrades.${inUseDetail}`
850
+ };
851
+ }
852
+ if (shouldStage()) {
853
+ return {
854
+ id: 'runtime_staging',
855
+ status: 'pass',
856
+ detail: `Staging active at ${runtimeRoot}. ${stagedList.length} staged version(s) found, ${stagedProcs.length} active process(es).${inUseDetail}`
857
+ };
858
+ }
859
+ if (process.env.GENEXUS_MCP_GATEWAY_EXE) {
860
+ return {
861
+ id: 'runtime_staging',
862
+ status: 'pass',
863
+ detail: `Fixed-path runtime override active via GENEXUS_MCP_GATEWAY_EXE (${process.env.GENEXUS_MCP_GATEWAY_EXE}). Staging bypassed.${inUseDetail}`
864
+ };
865
+ }
866
+ return {
867
+ id: 'runtime_staging',
868
+ status: 'pass',
869
+ detail: `Development checkout active. Runtime staging bypassed.${inUseDetail}`
870
+ };
871
+ } catch {
872
+ return {
873
+ id: 'runtime_staging',
874
+ status: 'not_applicable',
875
+ detail: 'Runtime staging inspection unavailable.'
876
+ };
877
+ }
878
+ }
879
+
816
880
  function redactConfig(cfg) {
817
881
  // Replace absolute paths with `<redacted:hash8>` so the structure is preserved
818
882
  // but filesystem layout, usernames, and KB names are not leaked. Hash is stable
@@ -1124,6 +1188,7 @@ async function handleDoctor(options, ctx) {
1124
1188
  checks.push({ id: 'gxpublic_com_registration', status: comCheck.status, detail: comCheck.detail });
1125
1189
  const ideLockCheck = buildLegacyIdeLockCheck(gxMajor);
1126
1190
  checks.push({ id: 'legacy_ide_lock', status: ideLockCheck.status, detail: ideLockCheck.detail });
1191
+ checks.push(buildRuntimeStagingCheck());
1127
1192
 
1128
1193
  // Client registration summary — one line answering "are my AI agents wired up?".
1129
1194
  const installedRows = clientRows.filter((r) => r.installed);
package/cli/index.js CHANGED
@@ -450,7 +450,25 @@ async function launchGateway(passthroughArgs, options) {
450
450
  return EXIT_CODES.ERROR;
451
451
  }
452
452
 
453
- const gatewayExePath = getGatewayExePath();
453
+ let gatewayExePath = getGatewayExePath();
454
+ try {
455
+ const { ensureStagedGateway } = require('./lib/runtime-stager');
456
+ const staged = ensureStagedGateway();
457
+ gatewayExePath = staged.gatewayExePath;
458
+ const cleanup = staged.cleanup;
459
+ if (cleanup && (cleanup.skipped?.length || cleanup.failed?.length || cleanup.processProbeAvailable === false)) {
460
+ const summary = [
461
+ cleanup.processProbeAvailable === false ? 'process probe unavailable' : null,
462
+ cleanup.skipped?.length ? `${cleanup.skipped.length} skipped` : null,
463
+ cleanup.failed?.length ? `${cleanup.failed.length} failed` : null
464
+ ].filter(Boolean).join(', ');
465
+ if (!options.quiet) launcherStderr.write(`[genexus-mcp] Runtime cleanup incomplete: ${summary}.\n`);
466
+ }
467
+ } catch (stageErr) {
468
+ if (!options.quiet) {
469
+ launcherStderr.write(`[genexus-mcp] Warning: Runtime staging failed (${stageErr.message}), falling back to direct binary.\n`);
470
+ }
471
+ }
454
472
  if (!require('fs').existsSync(gatewayExePath)) {
455
473
  const message = `[genexus-mcp] ERROR: Gateway executable not found at ${gatewayExePath}`;
456
474
  launcherStderr.write(`${message}\n`);
@@ -151,6 +151,7 @@ test('generateNeutralConfig produces valid shape without KB fields', () => {
151
151
  assert.equal(cfg.GeneXus.InstallationPath, 'C:\\GeneXus18');
152
152
  assert.ok(cfg.GeneXus.WorkerExecutable.endsWith(path.join('worker', 'GxMcp.Worker.exe')));
153
153
  assert.equal(cfg.Server.McpStdio, true);
154
+ assert.equal(cfg.Server.ToolProfile, 'standard');
154
155
  assert.equal(cfg.Server.TransportMode, undefined);
155
156
  assert.equal(cfg.Server.HttpPort, 0);
156
157
  assert.equal(cfg.Environment.ResolutionPolicy, 'strict');
package/cli/lib/config.js CHANGED
@@ -8,6 +8,7 @@ function generateConfig(gxPath, kbPath) {
8
8
  Server: {
9
9
  HttpPort: 5000,
10
10
  McpStdio: true,
11
+ ToolProfile: 'standard',
11
12
  SessionIdleTimeoutMinutes: 10,
12
13
  WorkerIdleTimeoutMinutes: 5,
13
14
  // Lean + terse defaults: the MCP client is an LLM agent that reads
@@ -33,6 +34,7 @@ function generateNeutralConfig(gxPath, { workerPath, gatewayMode = 'stdio-isolat
33
34
  Server: {
34
35
  HttpPort: 0,
35
36
  McpStdio: true,
37
+ ToolProfile: 'standard',
36
38
  WorkerSharingMode: 'isolated',
37
39
  SessionIdleTimeoutMinutes: 10,
38
40
  WorkerIdleTimeoutMinutes: 5,
@@ -657,9 +659,18 @@ function createConfigFile(kbPath, gxPath) {
657
659
  }
658
660
  const nextConfig = {
659
661
  ...baseConfig,
662
+ Server: { ...baseConfig.Server },
660
663
  Environment: { ...baseConfig.Environment, ...preservedEnv }
661
664
  };
662
665
 
666
+ if (existing) {
667
+ if (existing.Server && Object.prototype.hasOwnProperty.call(existing.Server, 'ToolProfile')) {
668
+ nextConfig.Server.ToolProfile = existing.Server.ToolProfile;
669
+ } else {
670
+ delete nextConfig.Server.ToolProfile;
671
+ }
672
+ }
673
+
663
674
  const changed = !existing || JSON.stringify(existing) !== JSON.stringify(nextConfig);
664
675
  if (changed) {
665
676
  writeFileAtomic(targetConfigPath, JSON.stringify(nextConfig, null, 2));