genexus-mcp 2.49.1 → 2.49.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,6 +84,13 @@ What you'll see (takes ~30 seconds first time, faster on re-runs):
84
84
  4. Prints a JSON snippet at the end — keep it in case you need to configure a client manually.
85
85
  5. Finishes with `🎉 You are all set!`.
86
86
 
87
+ On Windows, Antigravity is registered with the gateway executable bundled in the
88
+ current npm package when that artifact is available, so it skips the npx bootstrap
89
+ chain on every MCP handshake. That path follows the package in the npx cache; after
90
+ an upgrade, run `npx genexus-mcp@latest clients add --clients antigravity` again if
91
+ `genexus-mcp clients` reports a stale launcher. Other clients keep the `npx genexus-mcp@latest`
92
+ launcher unless you use the fixed-path installer below.
93
+
87
94
  ### Step 2 — Register the MCP in your AI client
88
95
 
89
96
  Step 1 auto-registers every supported client it detects, including Claude Desktop, Claude Code, Cursor, Antigravity, Gemini CLI, OpenCode, Codex CLI, and VS Code. If yours wasn't detected, copy the JSON snippet from Step 1 into your client's MCP config manually. See the [client setup guide](TROUBLESHOOTING.md#client-setup) if unsure where that file lives.
@@ -205,7 +212,7 @@ Auto-detected and auto-configured by the installer:
205
212
  | Claude Desktop | ✅ | Restart required after install |
206
213
  | Claude Code (CLI) | ✅ | Reload session |
207
214
  | Cursor | ✅ | Restart required |
208
- | Antigravity | ✅ | Restart required; detected even before its MCP config exists |
215
+ | Antigravity | ✅ | Direct packaged gateway; restart required; detected even before its MCP config exists |
209
216
  | Gemini CLI | ✅ | — |
210
217
  | OpenCode (CLI) | ✅ | Reads both direct and nested MCP layouts; restart required |
211
218
  | Codex CLI | ✅ | Writes `~/.codex/config.toml` |
@@ -229,6 +236,14 @@ Most install issues fall into a handful of buckets — see **[TROUBLESHOOTING.md
229
236
  - KB build errors / locked artifacts
230
237
  - Port 5000 already in use
231
238
  - Permissions on `%LOCALAPPDATA%\GenexusMCP\`
239
+ - Antigravity only shows `exit status 1` / `0xffffffff` with no useful stderr
240
+
241
+ When a stdio launcher fails before the client can retain stderr, the wrapper writes
242
+ the last failure to `%LOCALAPPDATA%\GenexusMCP\logs\last-stdio-error.txt`. It contains
243
+ the UTC timestamp, exit code, and bounded stderr tail. `genexus-mcp doctor` reports the
244
+ same path when a previous failure is present. Read it before changing the install or
245
+ using a global npm install; if the Antigravity launcher is stale, re-register it with
246
+ `npx genexus-mcp@latest clients add --clients antigravity`.
232
247
 
233
248
  Still stuck? [Open an issue](https://github.com/lennix1337/Genexus18MCP/issues) with the output of `npx genexus-mcp doctor --mcp-smoke`.
234
249
 
@@ -30,6 +30,7 @@ const {
30
30
  readClientCommandEntry,
31
31
  DEFAULT_MCP_SERVER_NAME
32
32
  } = require('../lib/config');
33
+ const { getStdioErrorLogPath } = require('../lib/stdio-diagnostics');
33
34
 
34
35
  function resolveClientIds(options) {
35
36
  if (!options || !options.clients) return null;
@@ -656,6 +657,8 @@ async function handleDoctor(options, ctx) {
656
657
  const toolDefPath = getToolDefinitionsPath();
657
658
  const toolDefsExists = fs.existsSync(toolDefPath);
658
659
  const gatewayExePath = getGatewayExePath();
660
+ const stdioErrorLogPath = getStdioErrorLogPath();
661
+ const stdioErrorLogExists = !!(stdioErrorLogPath && fs.existsSync(stdioErrorLogPath));
659
662
 
660
663
  let toolCount = 0;
661
664
  if (toolDefsExists) {
@@ -678,6 +681,13 @@ async function handleDoctor(options, ctx) {
678
681
  const checks = [
679
682
  { id: 'config_file', status: data.configFound ? 'pass' : 'fail', detail: data.configFound ? 'GX config file was found.' : 'GX config file is missing.' },
680
683
  { id: 'gateway_exe', status: data.gatewayExeFound ? 'pass' : 'fail', detail: data.gatewayExeFound ? 'Gateway executable is available.' : 'Gateway executable is missing.' },
684
+ {
685
+ id: 'stdio_error_log',
686
+ status: stdioErrorLogExists ? 'warn' : 'pass',
687
+ detail: stdioErrorLogExists
688
+ ? `Previous stdio launcher failure recorded at ${stdioErrorLogPath}. Read this file for the last exit code and stderr tail.`
689
+ : `Stdio launcher error log: ${stdioErrorLogPath || 'not available on this platform'}.`
690
+ },
681
691
  {
682
692
  id: 'gateway_exe_path_safety',
683
693
  status: riskyZone ? 'warn' : 'pass',
@@ -1153,6 +1163,18 @@ async function handleLayout(subcommand, options, ctx) {
1153
1163
  };
1154
1164
  }
1155
1165
 
1166
+ function buildClientLauncherHelp(patchResult) {
1167
+ const help = [];
1168
+ const patched = patchResult && Array.isArray(patchResult.patched) ? patchResult.patched : [];
1169
+ if (patched.includes('Antigravity') && process.platform === 'win32' && !process.env.GENEXUS_MCP_GATEWAY_EXE) {
1170
+ help.push('Antigravity uses the gateway executable bundled with this npm package when available; re-run `npx genexus-mcp@latest clients add --clients antigravity` after an upgrade if its package path is stale.');
1171
+ }
1172
+ if (patched.length > 0 && process.platform === 'win32' && !process.env.GENEXUS_MCP_GATEWAY_EXE) {
1173
+ help.push('Windows launcher paths may resolve under the npm cache and be blocked by AppLocker/SRP. Use scripts/install.ps1 for a stable whitelisted path.');
1174
+ }
1175
+ return help;
1176
+ }
1177
+
1156
1178
  function buildInteractiveInitHelp(patchResult) {
1157
1179
  const help = [];
1158
1180
  if (patchResult.failed && patchResult.failed.some((f) => f.reason && f.reason.includes('third-party or HTTP MCP server'))) {
@@ -1160,9 +1182,8 @@ function buildInteractiveInitHelp(patchResult) {
1160
1182
  }
1161
1183
  if (patchResult.patched.length === 0) {
1162
1184
  help.push('Set `GX_CONFIG_PATH` in your MCP client env to the generated config path.');
1163
- } else if (process.platform === 'win32' && !process.env.GENEXUS_MCP_GATEWAY_EXE) {
1164
- help.push('Windows + corporate AppLocker: the npx launcher resolves the gateway from %LOCALAPPDATA%\\npm-cache, which is commonly blocked. If clients fail with "Failed to connect" / Access denied, reinstall to a whitelisted path via scripts/install.ps1.');
1165
1185
  }
1186
+ help.push(...buildClientLauncherHelp(patchResult));
1166
1187
  return help;
1167
1188
  }
1168
1189
 
@@ -1440,9 +1461,7 @@ async function handleInit(options, ctx) {
1440
1461
  } else if (patchResult.patched.length === 0 && !shouldWriteClients) {
1441
1462
  help.push('Client patching was skipped (--no-write-clients). Register manually, or set GX_CONFIG_PATH to this config for global use.');
1442
1463
  }
1443
- if (patchResult.patched.length > 0 && process.platform === 'win32' && !process.env.GENEXUS_MCP_GATEWAY_EXE) {
1444
- help.push('Windows + corporate AppLocker: the npx launcher resolves the gateway from %LOCALAPPDATA%\\npm-cache, which is commonly blocked. If clients fail with "Failed to connect" / Access denied, reinstall to a whitelisted path via scripts/install.ps1.');
1445
- }
1464
+ help.push(...buildClientLauncherHelp(patchResult));
1446
1465
  if (verification.summary.fail > 0) {
1447
1466
  const failedIds = verification.checks
1448
1467
  .filter((c) => c.status === 'fail')
package/cli/index.js CHANGED
@@ -28,6 +28,7 @@ const {
28
28
  commandHelpMap
29
29
  } = require('./commands/axi');
30
30
  const { startBackgroundUpdateCheck, handleUpdate } = require('./lib/update-check');
31
+ const { createStderrTail, writeLastStdioError } = require('./lib/stdio-diagnostics');
31
32
 
32
33
  const EXIT_CODES = {
33
34
  OK: 0,
@@ -350,49 +351,91 @@ function writeAppLockerHint(stderr, gatewayExePath) {
350
351
  }
351
352
 
352
353
  async function launchGateway(passthroughArgs, options) {
354
+ const stderrTail = createStderrTail();
355
+ const launcherStderr = {
356
+ write(chunk) {
357
+ stderrTail.append(chunk);
358
+ if (!options.quiet) process.stderr.write(chunk);
359
+ return true;
360
+ }
361
+ };
362
+ const recordStdioFailure = ({ gatewayExePath, exitCode = null, signal = null, error = null } = {}) => {
363
+ const logPath = writeLastStdioError({
364
+ gatewayExePath,
365
+ exitCode,
366
+ signal,
367
+ error,
368
+ stderrTail: stderrTail.toString()
369
+ });
370
+ if (logPath && !options.quiet) {
371
+ process.stderr.write(`[genexus-mcp] Last stdio error saved to ${logPath}\n`);
372
+ }
373
+ };
374
+
353
375
  const setup = applyLauncherConfigOrExit({
354
376
  cwd: process.cwd(),
355
- stderr: process.stderr,
377
+ stderr: launcherStderr,
356
378
  quiet: options.quiet
357
379
  });
358
380
 
359
381
  if (!setup.ok) {
382
+ recordStdioFailure({ gatewayExePath: getGatewayExePath(), exitCode: EXIT_CODES.ERROR, error: 'Launcher setup failed.' });
360
383
  return EXIT_CODES.ERROR;
361
384
  }
362
385
 
363
386
  const gatewayExePath = getGatewayExePath();
364
387
  if (!require('fs').existsSync(gatewayExePath)) {
365
- if (!options.quiet) {
366
- process.stderr.write(`[genexus-mcp] ERROR: Gateway executable not found at ${gatewayExePath}\n`);
367
- }
388
+ const message = `[genexus-mcp] ERROR: Gateway executable not found at ${gatewayExePath}`;
389
+ launcherStderr.write(`${message}\n`);
390
+ recordStdioFailure({ gatewayExePath, exitCode: EXIT_CODES.ERROR, error: 'Gateway executable not found.' });
368
391
  return EXIT_CODES.ERROR;
369
392
  }
370
393
 
371
394
  return await new Promise((resolve) => {
372
- const child = spawn(gatewayExePath, passthroughArgs, {
373
- stdio: 'inherit',
374
- env: process.env,
375
- windowsHide: true
376
- });
395
+ let settled = false;
396
+ const finish = (code) => {
397
+ if (settled) return;
398
+ settled = true;
399
+ resolve(code);
400
+ };
401
+ const handleSpawnError = (err) => {
402
+ const message = `[genexus-mcp] ERROR: Failed to start gateway process: ${err.message}`;
403
+ launcherStderr.write(`${message}\n`);
404
+ const code = err && (err.code || err.errno);
405
+ const accessDenied = code === 'EACCES' || code === 'EPERM' || /access is denied|access denied|acesso negado/i.test(err.message || '');
406
+ if (accessDenied) writeAppLockerHint(launcherStderr, gatewayExePath);
407
+ recordStdioFailure({ gatewayExePath, error: err.message || 'Failed to start gateway process.' });
408
+ finish(EXIT_CODES.ERROR);
409
+ };
377
410
 
378
- child.on('error', (err) => {
379
- if (!options.quiet) {
380
- process.stderr.write(`[genexus-mcp] ERROR: Failed to start gateway process: ${err.message}\n`);
381
- const code = err && (err.code || err.errno);
382
- const accessDenied = code === 'EACCES' || code === 'EPERM' || /access is denied|access denied|acesso negado/i.test(err.message || '');
383
- if (accessDenied) {
384
- writeAppLockerHint(process.stderr, gatewayExePath);
385
- }
386
- }
387
- resolve(EXIT_CODES.ERROR);
388
- });
411
+ let child;
412
+ try {
413
+ child = spawn(gatewayExePath, passthroughArgs, {
414
+ // Keep stdout attached to the MCP protocol and tee stderr so a
415
+ // failed bootstrap remains available after a client drops it.
416
+ stdio: ['inherit', 'inherit', 'pipe'],
417
+ env: process.env,
418
+ windowsHide: true
419
+ });
420
+ } catch (err) {
421
+ handleSpawnError(err);
422
+ return;
423
+ }
424
+
425
+ if (child.stderr) {
426
+ child.stderr.on('data', (chunk) => {
427
+ stderrTail.append(chunk);
428
+ process.stderr.write(chunk);
429
+ });
430
+ }
431
+
432
+ child.once('error', handleSpawnError);
389
433
 
390
- child.on('exit', (code, signal) => {
391
- if (signal) {
392
- resolve(EXIT_CODES.ERROR);
393
- return;
434
+ child.once('close', (code, signal) => {
435
+ if (signal || code !== 0) {
436
+ recordStdioFailure({ gatewayExePath, exitCode: code, signal });
394
437
  }
395
- resolve(code || EXIT_CODES.OK);
438
+ finish(signal ? EXIT_CODES.ERROR : (code === null || code === undefined ? EXIT_CODES.ERROR : code));
396
439
  });
397
440
  });
398
441
  }
package/cli/lib/config.js CHANGED
@@ -444,13 +444,21 @@ function createConfigFile(kbPath, gxPath) {
444
444
  };
445
445
  }
446
446
 
447
- function getLauncher() {
447
+ function getLauncher(client = null, gatewayExePath = getGatewayExePath()) {
448
448
  // Set by scripts/install.ps1 for fixed-path corporate installs — clients
449
449
  // launch the gateway exe directly instead of resolving via the npx cache.
450
450
  const directExe = process.env.GENEXUS_MCP_GATEWAY_EXE;
451
- return directExe
452
- ? { command: directExe, args: [] }
453
- : { command: process.platform === 'win32' ? 'npx.cmd' : 'npx', args: ['-y', 'genexus-mcp@latest'] };
451
+ if (directExe) return { command: directExe, args: [] };
452
+
453
+ // Antigravity does not retain child stderr in its Language Server logs. When
454
+ // the packaged gateway is available, point it directly at that executable so
455
+ // each MCP handshake skips the npx bootstrap chain. The fallback keeps
456
+ // source-tree development and incomplete packages on the existing npx path.
457
+ if (client && client.preferDirectGateway && fs.existsSync(gatewayExePath)) {
458
+ return { command: gatewayExePath, args: [] };
459
+ }
460
+
461
+ return { command: process.platform === 'win32' ? 'npx.cmd' : 'npx', args: ['-y', 'genexus-mcp@latest'] };
454
462
  }
455
463
 
456
464
  const DEFAULT_MCP_SERVER_NAME = 'genexus18mcp';
@@ -576,6 +584,9 @@ function getClientConfigTargets() {
576
584
  name: 'Antigravity',
577
585
  format: 'mcpServers',
578
586
  path: resolveAntigravityConfigPath(home),
587
+ // The npm package includes the gateway exe. Prefer it for this client
588
+ // because Antigravity hides the stderr of an npx child when startup fails.
589
+ preferDirectGateway: true,
579
590
  // Unambiguous Antigravity markers only. ~/.gemini/config is NOT a
580
591
  // marker — gemini-cli can create ~/.gemini, and we'd false-positive;
581
592
  // it's still used as the write path (resolveAntigravityConfigPath)
@@ -699,7 +710,7 @@ function listSupportedClientIds() {
699
710
  // by an explicit path (a separator in the command) that no longer exists on disk
700
711
  // is the classic "Failed to connect / still on old version" cause after an
701
712
  // install dir moved or was cleaned — covers .exe, .bat, .cmd, .sh, extensionless.
702
- function clientCommandHealth(entry) {
713
+ function clientCommandHealth(entry, client = null) {
703
714
  if (!entry || !entry.command) return { stale: false, reason: null };
704
715
  const cmd = String(entry.command);
705
716
  if (/(^|[\\/])(npx|npx\.cmd|node|node\.exe|genexus-mcp|genexus-mcp\.cmd)$/i.test(cmd)) {
@@ -708,6 +719,12 @@ function clientCommandHealth(entry) {
708
719
  if (/[\\/]/.test(cmd) && !fs.existsSync(cmd)) {
709
720
  return { stale: true, reason: 'configured launcher does not exist on disk' };
710
721
  }
722
+ if (client && client.preferDirectGateway && /GxMcp\.Gateway\.exe$/i.test(cmd)) {
723
+ const currentPackageGateway = getGatewayExePath();
724
+ if (fs.existsSync(currentPackageGateway) && normalizeExePath(cmd) !== normalizeExePath(currentPackageGateway)) {
725
+ return { stale: true, reason: 'configured launcher points at a different package gateway; re-register the client' };
726
+ }
727
+ }
711
728
  return { stale: false, reason: null };
712
729
  }
713
730
 
@@ -723,7 +740,7 @@ function clientsStatus(opts = {}) {
723
740
  return targets.map((client) => {
724
741
  const det = detectClientInstalled(client);
725
742
  const entry = readClientCommandEntry(client, serverName);
726
- const health = clientCommandHealth(entry);
743
+ const health = clientCommandHealth(entry, client);
727
744
  const isThirdParty = entry && !isOurMcpEntry(entry.raw || entry, serverName);
728
745
  return {
729
746
  id: client.id,
@@ -771,7 +788,6 @@ function patchClientConfig(targetConfigPath, opts = {}) {
771
788
 
772
789
  const serverName = opts.serverName || DEFAULT_MCP_SERVER_NAME;
773
790
  const force = Boolean(opts.force);
774
- const launcher = getLauncher();
775
791
  const onlyExisting = opts.onlyExisting !== false;
776
792
  const candidates = filterClientTargets(getClientConfigTargets(), {
777
793
  ids: opts.ids,
@@ -800,7 +816,7 @@ function patchClientConfig(targetConfigPath, opts = {}) {
800
816
  }
801
817
  try {
802
818
  fs.mkdirSync(path.dirname(client.path), { recursive: true });
803
- applyClientEntry(client, launcher, targetConfigPath, { serverName, force });
819
+ applyClientEntry(client, getLauncher(client), targetConfigPath, { serverName, force });
804
820
  // Read-back: confirm the entry is actually present and the file still
805
821
  // parses, so a silently-corrupted write is reported as a failure.
806
822
  if (!readClientCommandEntry(client, serverName)) {
@@ -1187,10 +1203,6 @@ function removeCodexToml(filePath, { serverName = DEFAULT_MCP_SERVER_NAME } = {}
1187
1203
  return true;
1188
1204
  }
1189
1205
 
1190
- function stripCodexGenexusBlocks(content) {
1191
- return stripCodexServerBlocks(content, 'genexus');
1192
- }
1193
-
1194
1206
  function tomlString(value) {
1195
1207
  const s = String(value);
1196
1208
  return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
@@ -1519,6 +1531,7 @@ module.exports = {
1519
1531
  applyLauncherConfigOrExit,
1520
1532
  isPathLikelyAppLockerBlocked,
1521
1533
  normalizeExePath,
1534
+ getLauncher,
1522
1535
  readClientCommandEntry,
1523
1536
  isOurMcpEntry,
1524
1537
  DEFAULT_MCP_SERVER_NAME
@@ -0,0 +1,67 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ const MAX_STDERR_BYTES = 64 * 1024;
6
+
7
+ function getStdioErrorLogPath() {
8
+ if (process.platform !== 'win32') return null;
9
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
10
+ return path.join(localAppData, 'GenexusMCP', 'logs', 'last-stdio-error.txt');
11
+ }
12
+
13
+ function trimToMaxBytes(value, maxBytes = MAX_STDERR_BYTES) {
14
+ const raw = String(value || '');
15
+ const bytes = Buffer.from(raw, 'utf8');
16
+ if (bytes.length <= maxBytes) return raw;
17
+ return bytes.subarray(bytes.length - maxBytes).toString('utf8');
18
+ }
19
+
20
+ function createStderrTail(maxBytes = MAX_STDERR_BYTES) {
21
+ let value = '';
22
+ return {
23
+ append(chunk) {
24
+ if (chunk === undefined || chunk === null) return;
25
+ value = trimToMaxBytes(value + (Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk)), maxBytes);
26
+ },
27
+ toString() {
28
+ return value;
29
+ }
30
+ };
31
+ }
32
+
33
+ function oneLine(value, fallback = '') {
34
+ const text = value === undefined || value === null ? '' : String(value);
35
+ return text.replace(/\r?\n/g, ' ').trim() || fallback;
36
+ }
37
+
38
+ function writeLastStdioError({ gatewayExePath, exitCode = null, signal = null, error = null, stderrTail = '' } = {}) {
39
+ const logPath = getStdioErrorLogPath();
40
+ if (!logPath) return null;
41
+
42
+ const tail = trimToMaxBytes(stderrTail).trimEnd();
43
+ const lines = [
44
+ `timestampUtc: ${new Date().toISOString()}`,
45
+ `gateway: ${oneLine(gatewayExePath, '(unknown)')}`,
46
+ `exitCode: ${exitCode === null || exitCode === undefined ? 'n/a' : exitCode}`,
47
+ `signal: ${oneLine(signal, 'none')}`
48
+ ];
49
+ if (error) lines.push(`error: ${oneLine(error)}`);
50
+ lines.push('stderr:');
51
+ lines.push(tail || '(none captured)');
52
+ lines.push('');
53
+
54
+ try {
55
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
56
+ fs.writeFileSync(logPath, lines.join('\n'), 'utf8');
57
+ return logPath;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ module.exports = {
64
+ createStderrTail,
65
+ getStdioErrorLogPath,
66
+ writeLastStdioError
67
+ };
@@ -224,7 +224,7 @@ function detectInstallMethod() {
224
224
  if (process.env.GENEXUS_MCP_GATEWAY_EXE) {
225
225
  return { method: 'fixed-path', detail: process.env.GENEXUS_MCP_GATEWAY_EXE, evidence: [] };
226
226
  }
227
- const counts = { 'npx-latest': 0, 'npm-global': 0, 'fixed-path': 0 };
227
+ const counts = { 'npx-latest': 0, 'npm-global': 0, 'fixed-path': 0, 'package-direct': 0 };
228
228
  const evidence = [];
229
229
  try {
230
230
  for (const c of clientsStatus()) {
@@ -233,6 +233,7 @@ function detectInstallMethod() {
233
233
  let m;
234
234
  if (/(^|[\\/])npx(\.cmd)?$/i.test(cmd)) m = 'npx-latest';
235
235
  else if (/(^|[\\/])genexus-mcp(\.cmd)?$/i.test(cmd)) m = 'npm-global';
236
+ else if (c.id === 'antigravity' && /GxMcp\.Gateway\.exe$/i.test(cmd)) m = 'package-direct';
236
237
  else if (/[\\/]/.test(cmd)) m = 'fixed-path';
237
238
  else m = 'npm-global';
238
239
  counts[m] = (counts[m] || 0) + 1;
@@ -279,6 +280,20 @@ function upgradePlanFor(method, channel) {
279
280
  restartRequired: true
280
281
  };
281
282
  }
283
+ if (method === 'package-direct') {
284
+ const tag = channel && channel !== 'latest' ? `@${channel}` : '@latest';
285
+ return {
286
+ method,
287
+ auto: false,
288
+ steps: [
289
+ 'Antigravity launches the gateway executable bundled with the npm package, so each MCP handshake skips npx.',
290
+ `Refresh its pinned package path with: npx -y ${NPM_PACKAGE}${tag} clients add --clients antigravity`,
291
+ 'Then fully restart Antigravity.'
292
+ ],
293
+ applyCommand: null,
294
+ restartRequired: true
295
+ };
296
+ }
282
297
  // npm-global
283
298
  return {
284
299
  method: 'npm-global',
@@ -383,7 +398,11 @@ async function handleUpdate(options, ctx) {
383
398
  source: result.source || null,
384
399
  installMethod: install.method,
385
400
  autoUpdates: plan.auto,
386
- installCommand: plan.applyCommand ? `${plan.applyCommand.exe} ${plan.applyCommand.args.join(' ')}` : INSTALL_ONE_LINER,
401
+ installCommand: plan.applyCommand
402
+ ? `${plan.applyCommand.exe} ${plan.applyCommand.args.join(' ')}`
403
+ : (install.method === 'package-direct'
404
+ ? `npx -y ${NPM_PACKAGE}${channel && channel !== 'latest' ? `@${channel}` : '@latest'} clients add --clients antigravity`
405
+ : INSTALL_ONE_LINER),
387
406
  applied,
388
407
  fetched: true,
389
408
  clientDrift: mismatches
package/cli/run.js CHANGED
@@ -2,6 +2,7 @@
2
2
  const { main, EXIT_CODES } = require('./index');
3
3
  const { writeStructured } = require('./lib/output');
4
4
  const { operationalErrorEnvelope } = require('./commands/axi');
5
+ const { writeLastStdioError } = require('./lib/stdio-diagnostics');
5
6
 
6
7
  // Detect gateway-passthrough mode: no subcommand means we're forwarding stdio
7
8
  // as the MCP JSON-RPC channel. Writing a JSON envelope to stdout there would
@@ -13,7 +14,15 @@ const errorOut = isInteractiveCommand ? process.stdout : process.stderr;
13
14
 
14
15
  main(argv)
15
16
  .then((code) => process.exit(code))
16
- .catch(() => {
17
+ .catch((err) => {
18
+ if (!isInteractiveCommand) {
19
+ writeLastStdioError({
20
+ gatewayExePath: process.env.GENEXUS_MCP_GATEWAY_EXE || process.argv[1],
21
+ exitCode: EXIT_CODES.ERROR,
22
+ error: err && err.message ? err.message : 'Unhandled CLI failure.',
23
+ stderrTail: err && err.stack ? err.stack : ''
24
+ });
25
+ }
17
26
  const envelope = operationalErrorEnvelope('Unhandled CLI failure.', EXIT_CODES.ERROR);
18
27
  envelope.meta = { ...(envelope.meta || {}), command: 'runtime' };
19
28
  writeStructured(errorOut, envelope, 'toon');
package/cli/run.test.js CHANGED
@@ -6,7 +6,7 @@ const os = require('node:os');
6
6
  const fs = require('node:fs');
7
7
  const { renderOutput } = require('./lib/output');
8
8
  const { compareSemver, detectInstallMethod, upgradePlanFor } = require('./lib/update-check');
9
- const { detectClientInstalled, readJsonFileSafe } = require('./lib/config');
9
+ const { detectClientInstalled, readJsonFileSafe, getLauncher } = require('./lib/config');
10
10
 
11
11
  const cliPath = path.join(__dirname, 'run.js');
12
12
  const testGxPath = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-gx-'));
@@ -639,15 +639,69 @@ test('toon output key ordering is stable', () => {
639
639
  });
640
640
 
641
641
  test('quiet flag suppresses launcher stderr noise', () => {
642
- const result = runCli(['--quiet'], {
643
- env: {
644
- GX_CONFIG_PATH: '',
645
- GENEXUS_MCP_GATEWAY_EXE: 'C:\\missing\\nope.exe'
646
- }
647
- });
642
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-quiet-'));
643
+ try {
644
+ const result = runCli(['--quiet'], {
645
+ env: {
646
+ GX_CONFIG_PATH: '',
647
+ GENEXUS_MCP_GATEWAY_EXE: 'C:\\missing\\nope.exe',
648
+ LOCALAPPDATA: tempRoot
649
+ }
650
+ });
648
651
 
649
- assert.equal(result.status, 1);
650
- assert.equal(result.stderr.trim(), '');
652
+ assert.equal(result.status, 1);
653
+ assert.equal(result.stderr.trim(), '');
654
+ } finally {
655
+ fs.rmSync(tempRoot, { recursive: true, force: true });
656
+ }
657
+ });
658
+
659
+ test('stdio launcher writes a breadcrumb when the gateway is missing before spawn', () => {
660
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-stdio-missing-'));
661
+ try {
662
+ const configPath = path.join(tempRoot, 'config.json');
663
+ fs.writeFileSync(configPath, JSON.stringify({ Environment: { KBPath: tempRoot } }));
664
+ const missingGateway = path.join(tempRoot, 'missing', 'GxMcp.Gateway.exe');
665
+ const result = runCli([], {
666
+ env: {
667
+ GX_CONFIG_PATH: configPath,
668
+ GENEXUS_MCP_GATEWAY_EXE: missingGateway,
669
+ LOCALAPPDATA: tempRoot
670
+ }
671
+ });
672
+
673
+ assert.equal(result.status, 1);
674
+ const logPath = path.join(tempRoot, 'GenexusMCP', 'logs', 'last-stdio-error.txt');
675
+ assert.equal(fs.existsSync(logPath), true, 'pre-spawn failure should leave a diagnostic file');
676
+ const log = fs.readFileSync(logPath, 'utf8');
677
+ assert.match(log, /timestampUtc:/);
678
+ assert.match(log, /exitCode: 1/);
679
+ assert.match(log, /Gateway executable not found/);
680
+ assert.match(log, /missing[\\/]GxMcp\.Gateway\.exe/);
681
+ } finally {
682
+ fs.rmSync(tempRoot, { recursive: true, force: true });
683
+ }
684
+ });
685
+
686
+ test('doctor surfaces the stdio error breadcrumb when one exists', () => {
687
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-doctor-stdio-'));
688
+ try {
689
+ const logPath = path.join(tempRoot, 'GenexusMCP', 'logs', 'last-stdio-error.txt');
690
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
691
+ fs.writeFileSync(logPath, 'timestampUtc: 2026-08-31T00:00:00.000Z\nexitCode: 1\n');
692
+
693
+ const result = runCli(['doctor', '--format', 'json'], {
694
+ env: { ...sandboxHomeEnv(tempRoot), LOCALAPPDATA: tempRoot }
695
+ });
696
+ assert.equal(result.status, 0);
697
+ const parsed = JSON.parse(result.stdout);
698
+ const check = parsed.ok.checks.find((row) => row.id === 'stdio_error_log');
699
+ assert.ok(check);
700
+ assert.equal(check.status, 'warn');
701
+ assert.ok(check.detail.includes(logPath));
702
+ } finally {
703
+ fs.rmSync(tempRoot, { recursive: true, force: true });
704
+ }
651
705
  });
652
706
 
653
707
  test('update --help returns usage entry', () => {
@@ -737,6 +791,92 @@ test('clients list returns structured status with summary', () => {
737
791
  assert.equal(typeof row.registered, 'boolean');
738
792
  });
739
793
 
794
+ test('getLauncher prefers the packaged gateway for Antigravity only', () => {
795
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-launcher-'));
796
+ const gatewayPath = path.join(tempRoot, 'publish', 'GxMcp.Gateway.exe');
797
+ const previous = process.env.GENEXUS_MCP_GATEWAY_EXE;
798
+ delete process.env.GENEXUS_MCP_GATEWAY_EXE;
799
+ try {
800
+ fs.mkdirSync(path.dirname(gatewayPath), { recursive: true });
801
+ fs.writeFileSync(gatewayPath, 'gateway');
802
+
803
+ assert.deepEqual(getLauncher({ preferDirectGateway: true }, gatewayPath), {
804
+ command: gatewayPath,
805
+ args: []
806
+ });
807
+ assert.deepEqual(getLauncher({ preferDirectGateway: false }, gatewayPath), {
808
+ command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
809
+ args: ['-y', 'genexus-mcp@latest']
810
+ });
811
+ } finally {
812
+ if (previous === undefined) delete process.env.GENEXUS_MCP_GATEWAY_EXE;
813
+ else process.env.GENEXUS_MCP_GATEWAY_EXE = previous;
814
+ fs.rmSync(tempRoot, { recursive: true, force: true });
815
+ }
816
+ });
817
+
818
+ test('clients add selects a direct gateway for Antigravity and npx for other clients', () => {
819
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-client-launchers-'));
820
+ try {
821
+ const env = sandboxHomeEnv(tempRoot);
822
+ const cfgPath = path.join(tempRoot, 'config.json');
823
+ fs.writeFileSync(cfgPath, JSON.stringify({ Environment: { KBPath: tempRoot } }));
824
+ const result = runCli(['clients', 'add', '--clients', 'antigravity,cursor', '--format', 'json'], {
825
+ env: { ...env, GX_CONFIG_PATH: cfgPath, GENEXUS_MCP_GATEWAY_EXE: '' }
826
+ });
827
+ assert.equal(result.status, 0);
828
+
829
+ const antigravityPath = path.join(tempRoot, '.gemini', 'antigravity', 'mcp_config.json');
830
+ const cursorPath = path.join(tempRoot, '.cursor', 'mcp.json');
831
+ const antigravity = JSON.parse(fs.readFileSync(antigravityPath, 'utf8')).mcpServers.genexus18mcp;
832
+ const cursor = JSON.parse(fs.readFileSync(cursorPath, 'utf8')).mcpServers.genexus18mcp;
833
+ const packagedGateway = path.join(__dirname, '..', 'publish', 'GxMcp.Gateway.exe');
834
+
835
+ assert.equal(cursor.command, 'npx.cmd');
836
+ assert.deepEqual(cursor.args, ['-y', 'genexus-mcp@latest']);
837
+ if (fs.existsSync(packagedGateway)) {
838
+ assert.equal(antigravity.command, packagedGateway);
839
+ assert.deepEqual(antigravity.args, []);
840
+ } else {
841
+ assert.equal(antigravity.command, 'npx.cmd', 'source checkouts without publish artifacts retain the fallback');
842
+ }
843
+ } finally {
844
+ fs.rmSync(tempRoot, { recursive: true, force: true });
845
+ }
846
+ });
847
+
848
+ test('clients add uses the unified Antigravity config when it already exists', () => {
849
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-antigravity-unified-'));
850
+ try {
851
+ const env = sandboxHomeEnv(tempRoot);
852
+ const configPath = path.join(tempRoot, 'config.json');
853
+ const unifiedPath = path.join(tempRoot, '.gemini', 'config', 'mcp_config.json');
854
+ fs.writeFileSync(configPath, JSON.stringify({ Environment: { KBPath: tempRoot } }));
855
+ fs.mkdirSync(path.dirname(unifiedPath), { recursive: true });
856
+ fs.writeFileSync(unifiedPath, JSON.stringify({ mcpServers: { unrelated: { command: 'keep-me' } } }));
857
+
858
+ const result = runCli(['clients', 'add', '--clients', 'antigravity', '--format', 'json'], {
859
+ env: { ...env, GX_CONFIG_PATH: configPath, GENEXUS_MCP_GATEWAY_EXE: '' }
860
+ });
861
+ assert.equal(result.status, 0);
862
+
863
+ const written = JSON.parse(fs.readFileSync(unifiedPath, 'utf8'));
864
+ const entry = written.mcpServers.genexus18mcp;
865
+ const packagedGateway = path.join(__dirname, '..', 'publish', 'GxMcp.Gateway.exe');
866
+ assert.ok(written.mcpServers.unrelated);
867
+ if (fs.existsSync(packagedGateway)) {
868
+ assert.equal(entry.command, packagedGateway);
869
+ assert.deepEqual(entry.args, []);
870
+ } else {
871
+ assert.equal(entry.command, 'npx.cmd');
872
+ assert.deepEqual(entry.args, ['-y', 'genexus-mcp@latest']);
873
+ }
874
+ assert.equal(fs.existsSync(path.join(tempRoot, '.gemini', 'antigravity', 'mcp_config.json')), false);
875
+ } finally {
876
+ fs.rmSync(tempRoot, { recursive: true, force: true });
877
+ }
878
+ });
879
+
740
880
  test('clients add registers a client into a sandbox home with backup + atomic write', () => {
741
881
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-clients-'));
742
882
  const env = sandboxHomeEnv(tempRoot);
@@ -929,6 +1069,36 @@ test('clients list flags a registered command pointing at a missing launcher as
929
1069
  fs.rmSync(tempRoot, { recursive: true, force: true });
930
1070
  });
931
1071
 
1072
+ test('clients list marks an Antigravity launcher from an old package cache as stale', () => {
1073
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-antigravity-stale-'));
1074
+ try {
1075
+ const env = sandboxHomeEnv(tempRoot);
1076
+ const currentGateway = path.join(tempRoot, 'current', 'GxMcp.Gateway.exe');
1077
+ const oldGateway = path.join(tempRoot, 'old-cache', 'GxMcp.Gateway.exe');
1078
+ fs.mkdirSync(path.dirname(currentGateway), { recursive: true });
1079
+ fs.mkdirSync(path.dirname(oldGateway), { recursive: true });
1080
+ fs.writeFileSync(currentGateway, 'current');
1081
+ fs.writeFileSync(oldGateway, 'old');
1082
+
1083
+ const antigravityCfg = path.join(tempRoot, '.gemini', 'antigravity', 'mcp_config.json');
1084
+ fs.mkdirSync(path.dirname(antigravityCfg), { recursive: true });
1085
+ fs.writeFileSync(antigravityCfg, JSON.stringify({
1086
+ mcpServers: { genexus18mcp: { command: oldGateway, args: [] } }
1087
+ }, null, 2));
1088
+
1089
+ const result = runCli(['clients', '--format', 'json'], {
1090
+ env: { ...env, GENEXUS_MCP_GATEWAY_EXE: currentGateway }
1091
+ });
1092
+ assert.equal(result.status, 0);
1093
+ const parsed = JSON.parse(result.stdout);
1094
+ const antigravity = parsed.ok.clients.find((client) => client.id === 'antigravity');
1095
+ assert.equal(antigravity.commandStale, true);
1096
+ assert.match(antigravity.commandStaleReason, /different package gateway/);
1097
+ } finally {
1098
+ fs.rmSync(tempRoot, { recursive: true, force: true });
1099
+ }
1100
+ });
1101
+
932
1102
  test('readJsonFileSafe parses JSONC without corrupting string values containing commas', () => {
933
1103
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-jsonc2-'));
934
1104
  const f = path.join(tempRoot, 'mcp.json');
@@ -1047,6 +1217,11 @@ test('upgradePlanFor encodes the per-method upgrade strategy', () => {
1047
1217
  const fixed = upgradePlanFor('fixed-path', 'latest');
1048
1218
  assert.equal(fixed.auto, false);
1049
1219
  assert.equal(fixed.applyCommand, null, 'fixed-path has no npm apply; uses the installer');
1220
+
1221
+ const direct = upgradePlanFor('package-direct', 'latest');
1222
+ assert.equal(direct.auto, false);
1223
+ assert.equal(direct.applyCommand, null);
1224
+ assert.ok(direct.steps.join(' ').includes('clients add --clients antigravity'));
1050
1225
  });
1051
1226
 
1052
1227
  test('gateway passthrough remains intact when no AXI subcommand is used', () => {
@@ -1070,6 +1245,36 @@ test('gateway passthrough remains intact when no AXI subcommand is used', () =>
1070
1245
  fs.rmSync(tempRoot, { recursive: true, force: true });
1071
1246
  });
1072
1247
 
1248
+ test('stdio launcher persists the last child stderr when the gateway exits non-zero', () => {
1249
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-stdio-crash-'));
1250
+ try {
1251
+ const configPath = path.join(tempRoot, 'config.json');
1252
+ const fakeGateway = path.join(tempRoot, 'fake-gateway.js');
1253
+ fs.writeFileSync(configPath, JSON.stringify({ Environment: { KBPath: tempRoot } }));
1254
+ fs.writeFileSync(fakeGateway, 'process.stderr.write("gateway bootstrap failed\\nsecond line\\n"); process.exit(7);');
1255
+
1256
+ const result = runCli([fakeGateway], {
1257
+ env: {
1258
+ GX_CONFIG_PATH: configPath,
1259
+ GENEXUS_MCP_GATEWAY_EXE: process.execPath,
1260
+ LOCALAPPDATA: tempRoot
1261
+ }
1262
+ });
1263
+
1264
+ assert.equal(result.status, 7);
1265
+ assert.match(result.stderr, /gateway bootstrap failed/);
1266
+ const logPath = path.join(tempRoot, 'GenexusMCP', 'logs', 'last-stdio-error.txt');
1267
+ assert.equal(fs.existsSync(logPath), true, 'child crash should leave a diagnostic file');
1268
+ const log = fs.readFileSync(logPath, 'utf8');
1269
+ assert.match(log, /timestampUtc:/);
1270
+ assert.match(log, /exitCode: 7/);
1271
+ assert.match(log, /gateway bootstrap failed/);
1272
+ assert.match(log, /second line/);
1273
+ } finally {
1274
+ fs.rmSync(tempRoot, { recursive: true, force: true });
1275
+ }
1276
+ });
1277
+
1073
1278
  test('npm package contains all bin, postinstall and required runtime files', () => {
1074
1279
  const pkgPath = path.join(__dirname, '..', 'package.json');
1075
1280
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
@@ -1099,6 +1304,17 @@ test('npm package contains all bin, postinstall and required runtime files', ()
1099
1304
  );
1100
1305
  }
1101
1306
  }
1307
+
1308
+ const requiredFiles = ['cli/lib/stdio-diagnostics.js', 'scripts/test-one.js'];
1309
+ for (const relative of requiredFiles) {
1310
+ assert.ok(fs.existsSync(path.join(__dirname, '..', relative)), `runtime file ${relative} must exist on disk`);
1311
+ const normalized = relative.replace(/\\/g, '/');
1312
+ const covered = (pkg.files || []).some((pattern) => {
1313
+ const normalizedPattern = pattern.replace(/\\/g, '/');
1314
+ return normalized === normalizedPattern || normalized.startsWith(normalizedPattern.endsWith('/') ? normalizedPattern : `${normalizedPattern}/`);
1315
+ });
1316
+ assert.ok(covered, `runtime file '${relative}' must be included by package.json files`);
1317
+ }
1102
1318
  });
1103
1319
 
1104
1320
  test('isOurMcpEntry identifies third-party / HTTP servers vs our own entry', () => {
@@ -44,6 +44,22 @@ Recommended flags:
44
44
  - `--full` only when needed.
45
45
  - `--quiet` in automation contexts.
46
46
 
47
+ ## Stdio startup failures
48
+
49
+ If an MCP client reports only `exit status 1` or `0xffffffff`, inspect the wrapper's
50
+ last-failure breadcrumb on Windows:
51
+
52
+ ```powershell
53
+ Get-Content "$env:LOCALAPPDATA\GenexusMCP\logs\last-stdio-error.txt"
54
+ ```
55
+
56
+ It contains the UTC timestamp, exit code, and a bounded stderr tail from the gateway
57
+ bootstrap. For Antigravity, `init` and `clients add --clients antigravity` use the
58
+ current package's gateway executable directly when available; if `genexus-mcp clients`
59
+ marks that path stale, re-register it with `npx genexus-mcp@latest clients add --clients antigravity`.
60
+ Do not infer an npx incompatibility or switch to a global npm install before reading
61
+ this file.
62
+
47
63
  ## MCP Contract (LLM-facing)
48
64
 
49
65
  For `tools/call`, parse `result.content[0].text` as JSON.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genexus-mcp",
3
- "version": "2.49.1",
3
+ "version": "2.49.2",
4
4
  "mcpName": "io.github.lennix1337/genexus",
5
5
  "description": "GeneXus 18 MCP server — read, edit, and analyze GeneXus knowledge base objects (transactions, web panels, procedures, SDTs) directly from Claude, Cursor, and other AI agents over the Model Context Protocol.",
6
6
  "keywords": [
@@ -31,7 +31,8 @@
31
31
  ],
32
32
  "scripts": {
33
33
  "test": "node --test cli/run.test.js",
34
- "lint": "eslint cli scripts eslint.config.js",
34
+ "lint": "eslint cli scripts eslint.config.js --max-warnings=0",
35
+ "test:one": "node scripts/test-one.js",
35
36
  "prepack": "node scripts/clean-package.js",
36
37
  "postinstall": "node scripts/verify-install.js"
37
38
  },
@@ -42,7 +43,8 @@
42
43
  "cli",
43
44
  "publish",
44
45
  "docs/llm_cli_mcp_playbook.md",
45
- "scripts/verify-install.js"
46
+ "scripts/verify-install.js",
47
+ "scripts/test-one.js"
46
48
  ],
47
49
  "os": [
48
50
  "win32"
@@ -7,7 +7,7 @@
7
7
  "targets": {
8
8
  ".NETCoreApp,Version=v8.0": {},
9
9
  ".NETCoreApp,Version=v8.0/win-x64": {
10
- "GxMcp.Gateway/2.49.1": {
10
+ "GxMcp.Gateway/2.49.2": {
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/2.49.1": {
69
+ "GxMcp.Gateway/2.49.2": {
70
70
  "type": "project",
71
71
  "serviceable": false,
72
72
  "sha512": ""
Binary file
Binary file
@@ -3,13 +3,13 @@
3
3
  "InstallationPath": "C:\\\\Program Files (x86)\\\\GeneXus\\\\GeneXus18",
4
4
  "WorkerExecutable": "C:\\Projetos\\Genexus18MCP\\publish\\\\worker\\\\GxMcp.Worker.exe"
5
5
  },
6
+ "Logging": {
7
+ "Level": "Debug",
8
+ "Path": "logs"
9
+ },
6
10
  "Environment": {
7
11
  "KBPath": "C:\\\\KBs\\\\YourKB"
8
12
  },
9
- "Logging": {
10
- "Path": "logs",
11
- "Level": "Debug"
12
- },
13
13
  "Server": {
14
14
  "HttpPort": 5000,
15
15
  "McpStdio": true
Binary file
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ const { spawnSync } = require('node:child_process');
3
+ const path = require('node:path');
4
+
5
+ const pattern = process.argv.slice(2).join(' ').trim();
6
+ if (!pattern) {
7
+ process.stderr.write('Usage: npm run test:one -- "test name pattern"\n');
8
+ process.exit(2);
9
+ }
10
+
11
+ const result = spawnSync(process.execPath, [
12
+ '--test',
13
+ '--test-name-pattern',
14
+ pattern,
15
+ path.join(__dirname, '..', 'cli', 'run.test.js')
16
+ ], { stdio: 'inherit' });
17
+
18
+ process.exit(result.status === null ? 1 : result.status);