genexus-mcp 2.49.1 β†’ 2.50.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.
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.50.0",
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.50.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/2.49.1": {
69
+ "GxMcp.Gateway/2.50.0": {
70
70
  "type": "project",
71
71
  "serviceable": false,
72
72
  "sha512": ""
Binary file
Binary file
@@ -1,17 +1,17 @@
1
1
  {
2
+ "Logging": {
3
+ "Level": "Debug",
4
+ "Path": "logs"
5
+ },
6
+ "Server": {
7
+ "McpStdio": true,
8
+ "HttpPort": 5000
9
+ },
2
10
  "GeneXus": {
3
- "InstallationPath": "C:\\\\Program Files (x86)\\\\GeneXus\\\\GeneXus18",
4
- "WorkerExecutable": "C:\\Projetos\\Genexus18MCP\\publish\\\\worker\\\\GxMcp.Worker.exe"
11
+ "WorkerExecutable": "C:\\Projetos\\Genexus18MCP\\publish\\\\worker\\\\GxMcp.Worker.exe",
12
+ "InstallationPath": "C:\\\\Program Files (x86)\\\\GeneXus\\\\GeneXus18"
5
13
  },
6
14
  "Environment": {
7
15
  "KBPath": "C:\\\\KBs\\\\YourKB"
8
- },
9
- "Logging": {
10
- "Path": "logs",
11
- "Level": "Debug"
12
- },
13
- "Server": {
14
- "HttpPort": 5000,
15
- "McpStdio": true
16
16
  }
17
17
  }
@@ -8,7 +8,7 @@
8
8
  {"name":"genexus_edit","description":"Edit object part. name or targets (exclusive). mode: full|patch|ops. dryRun first. async=true returns an operationId for lifecycle polling. Text payloads for Source/Rules/Events/Code must contain real line breaks; literal backslash sequences \\r\\n, \\n, or \\r are rejected before persistence with LiteralLineBreaksDetected. For WWP edit host's PatternInstance NOT the parent WebForm (gets overwritten on reapply). Uncertain about a property/method/event? β†’ resources/read uri=genexus://kb/skills/navigation (or gam-integrated-security / sd-panel-mobile / webpanel-events) first.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"part":{"type":"string"},"parts":{"type":"array","description":"Multi-part edit on a single object. Each item: {part, content, mode?}. Saves the object once atomically. Text content must use real line breaks; literal backslash sequences \\r\\n, \\n, or \\r are rejected before persistence.","items":{"type":"object","properties":{"part":{"type":"string"},"content":{"type":"string"},"mode":{"type":"string"}},"required":["part","content"]}},"mode":{"type":"string","enum":["full","patch","ops"]},"content":{"type":"string"},"ops":{"type":"array","description":"GeneXus semantic ops (NOT RFC 6902 JSON-Patch). Each item: {op, args}. Supported ops: set_attribute, add_attribute, remove_attribute (Transaction); add_rule, remove_rule (Transaction/Procedure/WebPanel); set_property (any kind). Use mode=patch for textual find/replace.","items":{"type":"object","properties":{"op":{"type":"string","enum":["set_attribute","add_attribute","remove_attribute","add_rule","remove_rule","set_property"]},"args":{"type":"object","description":"Op-specific args. set_attribute: {name, type?, description?}. add_attribute: {name, type, ...}. add_rule: {rule}. set_property: {name, value}."}},"required":["op"]}},"patch":{"description":"Shorthand for {find,replace}. Equivalent to operation=Replace + context=find + content=replace."},"context":{"type":"string","description":"Exact existing text to find (Replace) or anchor after (Insert_After). Required for both. For text parts, send real line breaks; literal backslash sequences \\r\\n, \\n, or \\r are rejected before persistence."},"operation":{"type":"string","enum":["Replace","Insert_After","Append"]},"expectedCount":{"type":"integer"},"replaceAll":{"type":"boolean","description":"patch mode only. Apply to ALL occurrences instead of requiring expectedCount to match exactly."},"dryRun":{"type":"boolean"},"async":{"type":"boolean","description":"Run as a background job. Result returns operationId/job_id for genexus_lifecycle polling."},"estimated_seconds":{"type":"integer","description":"When async=true, expected runtime used for progress/job metadata (default 30)."},"verifyRollback":{"type":"boolean"},"targets":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"content":{"type":"string"}},"required":["name","content"]}},"type":{"type":"string"},"return_post_state":{"type":"boolean","description":"Omit post_state.diff (default true)."},"verbose":{"type":"boolean","description":"Add Β±15-line slices to post_state."},"validate":{"type":"string","enum":["strict","best-effort","only"],"description":"strict (default) aborts on first error and post-write-verifies visual/PatternInstance writes (re-reads + diffs the persisted XML). best-effort applies what compiles and SKIPS the post-write XML re-read/diff (faster on large WebForm/PatternInstance writes; build to confirm). only runs in-memory, no persist."},"validationMode":{"type":"string","enum":["specify"],"description":"Issue #60: run the inline Specify pass (Spec+Gen, no Compile) against the edited object right after the write, returning structured spc*/gen* diagnostics in the same call. Combines with rollbackOnFailure."},"rollbackOnFailure":{"type":"boolean","description":"Explicit rollback only: restore a valid pre-write snapshot when post-save verification or validationMode=specify fails. No rollback occurs when false or omitted."},"verifyMode":{"type":"string","enum":["semantic","normalized","exact"],"default":"normalized","description":"Post-save text comparison for patch mode. Source/Rules default to normalized (EOL, encoding marker, trailing whitespace and repeated blank lines); exact preserves logical text but treats CRLF/LF as the same SDK Source rendering; semantic also tolerates SDK casing/spacing format changes. Comment-only Replace writes require baseVersion and return CommentOnlyWriteNotPersisted on a divergent forced re-read. The part is always re-read after Save."},"visualVerify":{"type":"boolean","default":false,"description":"Post-edit headless screenshot + pixel-diff vs baseline. See tool-help."},"baseVersion":{"type":"string","description":"Optimistic concurrency: pass the versionToken from the genexus_read this edit is based on. If the object changed since (e.g. edited in the IDE), the write is refused with VersionConflict instead of overwriting the newer version. Omit to skip the check."},"expectedVersion":{"type":"string","description":"Alias of baseVersion. The write is refused when the current version no longer matches."},"autoDeclareVariables":{"type":"boolean","default":false,"description":"When true, scans updated source for newly referenced &Variable tokens and auto-declares them in VariablesPart."},"kb":{"type":"string","description":"KB alias."}},"examples":[{"name":"Customer","mode":"patch","part":"Source","patch":{"find":"old","replace":"new"}},{"name":"Customer","mode":"patch","part":"Rules","patch":{"find":"error(","replace":"msg("},"dryRun":true}]},"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":false}},
9
9
  {"name":"genexus_inspect","description":"Use this for a compact object snapshot: metadata, variables, structure, signature and navigation context. Don't use it when you need full Source or exact part bytes; use genexus_read for that. runtimeIds needs a prior build.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"include":{"type":"array","items":{"type":"string","enum":["metadata","variables","signature","structure","parts","controls","events_repertoire","callers","runtimeIds"]}},"projection":{"type":"string","enum":["minimal","standard","verbose"],"description":"Token budget. minimal=name/type/lastUpdate; standard=default (vars≀40 name+type, source heads 1200c); verbose=full vars + 8000c heads."},"type":{"type":"string"},"kb":{"type":"string","description":"KB alias."}},"required":["name"],"examples":[{"name":"Customer"},{"name":"Customer","include":["variables","signature"]}]},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},
10
10
  {"name":"genexus_analyze","description":"Use this for cross-object semantic analysis. mode=context returns 360Β° task context in 1 call (object + called procedures' parm signatures + referenced tables' schemas/PKs + SDTs + callers). Other modes: impact, callers, hierarchy, navigation, linter, summary. Requires Ready index for impact/callers.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"mode":{"type":"string","enum":["context","linter","navigation","hierarchy","impact","cross_platform_impact","callers","event_flow","data_context","ui_context","pattern_metadata","summary","dependency_heatmap","code_metrics","kb_stats","table_relations"]},"format":{"type":"string","enum":["json","ascii"],"description":"mode=dependency_heatmap: ascii adds rendered bar chart."},"kb":{"type":"string","description":"KB alias."},"waitForIndex":{"type":"boolean","description":"mode=impact: block up to 30s for Ready index (default true).","default":true}},"required":["mode"],"examples":[{"name":"Customer","mode":"impact"},{"name":"Customer","mode":"summary"}]},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},
11
- {"name":"genexus_lifecycle","description":"Build, validate, index, or poll the KB. action=specify runs a spec-check (Spec+Gen only, no Compile/deploy) returning spc*/gen* diagnostics for the target fast (catch spec errors without a full build). action=build with mode=compile_check spec+gen+compiles the target(s) PLUS their transitive callers and skips the KB-wide DeveloperMenu regen (the dominant build-all cost) -- a fast 'did my edit break the build?' check that requires a target. Long ops are async with operationId. dryRun=true (build/rebuild/index) returns the build plan or index plan without executing. Build results carry generateEvidence + effective_status=SucceededWithGaps when a Succeeded build emitted no fresh .cs (don't trust Status=Succeeded alone); a 2nd concurrent build is refused (BuildAlreadyRunning). See genexus://kb/tool-help/genexus_lifecycle for the build-evidence checklist, actions, and target formats.","inputSchema":{"type":"object","properties":{"action":{"type":"string","enum":["build","cancel","specify","rebuild","reorg","reorg_preview","validate","validate-kb","sync","index","status","result","snapshots-list","snapshots-restore"]},"mode":{"type":"string","enum":["compile_check"],"description":"With action=build: compile_check spec+gen+compiles target(s) plus their transitive callers and SKIPS the KB-wide DeveloperMenu regen (the dominant build-all cost). Fast 'did my edit break the build?' check; requires target."},"target":{"type":"string","description":"Object name(s), taskId, job_id, or op:<id>. Build accepts CSV ('Foo,Bar')."},"code":{"type":"string"},"limit":{"type":"integer"},"snapshotPath":{"type":"string"},"estimated_seconds":{"type":"integer","description":"Build: <20 sync, >=20 async (default 60)."},"wait_seconds":{"type":"integer","description":"Status/build long-poll cap, 0-600s."},"wait_until_done":{"type":"boolean","description":"Build/rebuild: block in one turn until terminal (up to wait_seconds, default 600)."},"wait":{"type":"integer","description":"Status block, 0-600s. With a build taskId (or since): event-driven, returns on change. On index status with no since: blocks until index is Ready."},"since":{"type":"string","description":"Status: prior _meta.snapshot for chained waits."},"compact":{"type":"boolean","description":"Status: counts + top-10 errors (default true)."},"force":{"type":"boolean","description":"Index: full SDK rescan (clears snapshot)."},"dryRun":{"type":"boolean","default":false,"description":"build/rebuild/index: returns the plan (object list / index entries) without executing."},"includeCallees":{"type":"string","enum":["none","direct","transitive"],"description":"Build: expand call graph so callees compile first (default transitive)."},"callers":{"type":"boolean","description":"compile_check: expand transitive callers of the target (default true). Set false for a target-only check when the target is a base transaction/BC with a huge caller closure."},"callerCap":{"type":"integer","description":"compile_check: max callers to pull into the check (default 40). Closure beyond this is truncated and flagged."},"deploy":{"type":"boolean","description":"Build: force the full deploy (Theme/Image/Style/Module copy to web/bin + WebAppConfig) so the built object is runnable, not just compiled. Default false (fast compile-only path). Slower; use when you need to run/preview the object."},"buildPlanCap":{"type":"integer","description":"Build: max nodes before BuildPlanTooLarge (default 200)."},"kb":{"type":"string","description":"KB alias."}},"required":["action"],"examples":[{"action":"build"},{"action":"status","target":"op:abc123"},{"action":"build","mode":"compile_check","target":"MyObject"}]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false}},
11
+ {"name":"genexus_lifecycle","description":"Build, validate, index, or poll the KB. action=specify runs a spec-check (Spec+Gen only, no Compile/deploy) returning spc*/gen* diagnostics for the target fast (catch spec errors without a full build). action=build with mode=compile_check spec+gen+compiles the target(s) PLUS their transitive callers and skips the KB-wide DeveloperMenu regen (the dominant build-all cost) -- a fast 'did my edit break the build?' check that requires a target. Long ops are async with operationId. dryRun=true (build/rebuild/index) returns the build plan or index plan without executing. Build results carry generateEvidence + effective_status=SucceededWithGaps when a Succeeded build emitted no fresh .cs (don't trust Status=Succeeded alone); a 2nd concurrent build is refused (BuildAlreadyRunning). See genexus://kb/tool-help/genexus_lifecycle for the build-evidence checklist, actions, and target formats.","inputSchema":{"type":"object","properties":{"action":{"type":"string","enum":["build","cancel","specify","rebuild","reorg","reorg_preview","validate","validate-kb","sync","index","status","result","snapshots-list","snapshots-restore"]},"mode":{"type":"string","enum":["compile_check"],"description":"With action=build: compile_check spec+gen+compiles target(s) plus their transitive callers and SKIPS the KB-wide DeveloperMenu regen (the dominant build-all cost). Fast 'did my edit break the build?' check; requires target."},"target":{"type":"string","description":"Object name(s), taskId, job_id, or op:<id>. Build accepts CSV ('Foo,Bar')."},"code":{"type":"string"},"limit":{"type":"integer"},"snapshotPath":{"type":"string"},"estimated_seconds":{"type":"integer","description":"Build: <20 sync, >=20 async (default 60)."},"wait_seconds":{"type":"integer","description":"Status/build long-poll cap, 0-600s."},"wait_until_done":{"type":"boolean","description":"Build/rebuild: block in one turn until terminal (up to wait_seconds, default 600)."},"wait":{"type":"integer","description":"Status block, 0-600s. With a build taskId (or since): event-driven, returns on change. On index status with no since: blocks until index is Ready."},"since":{"type":"string","description":"Status: prior _meta.snapshot for chained waits."},"compact":{"type":"boolean","description":"Status: counts + top-10 errors (default true)."},"force":{"type":"boolean","description":"Index: full SDK rescan (clears snapshot)."},"dryRun":{"type":"boolean","default":false,"description":"build/rebuild/index: returns the plan (object list / index entries) without executing."},"environment":{"type":"string","description":"Build: target environment name/folder. Switches active environment before building."},"includeCallees":{"type":"string","enum":["none","direct","transitive"],"description":"Build: expand call graph so callees compile first (default transitive)."},"callers":{"type":"boolean","description":"compile_check: expand transitive callers of the target (default true). Set false for a target-only check when the target is a base transaction/BC with a huge caller closure."},"callerCap":{"type":"integer","description":"compile_check: max callers to pull into the check (default 40). Closure beyond this is truncated and flagged."},"deploy":{"type":"boolean","description":"Build: force the full deploy (Theme/Image/Style/Module copy to web/bin + WebAppConfig) so the built object is runnable, not just compiled. Default false (fast compile-only path). Slower; use when you need to run/preview the object."},"buildPlanCap":{"type":"integer","description":"Build: max nodes before BuildPlanTooLarge (default 200)."},"kb":{"type":"string","description":"KB alias."}},"required":["action"],"examples":[{"action":"build"},{"action":"status","target":"op:abc123"},{"action":"build","mode":"compile_check","target":"MyObject"}]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false}},
12
12
  {"name":"genexus_test","description":"Execute native GeneXus tests (GXtest).","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"kb":{"type":"string","description":"KB alias."}},"required":["name"],"examples":[{"name":"MyTestProc"}]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":true}},
13
13
  {"name":"genexus_sdk_probe","description":"Dump SDK surface (types/methods/props) to docs/sdk-probe/. Use when hunting for entry points.","inputSchema":{"type":"object","properties":{"outputDir":{"type":"string","description":"Absolute path. Default <repo>/docs/sdk-probe/."}},"examples":[{}]},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},
14
14
  {"name":"genexus_worker_reload","description":"Reload worker. modes: soft (drain+respawn), hard (copy sourceDir+respawn). force=true: gateway kills directly. See tool-help.","inputSchema":{"type":"object","properties":{"mode":{"type":"string","enum":["soft","hard"]},"force":{"type":"boolean","description":"Bypass drain; gateway kills + respawns. Use when worker is wedged."},"sourceDir":{"type":"string","description":"Required for mode=hard."},"drainTimeoutMs":{"type":"integer","description":"Soft only; default 30000."}},"examples":[{"mode":"soft"},{"mode":"hard","sourceDir":"C:\\Projetos\\Genexus18MCP\\src\\GxMcp.Worker\\bin\\Debug"}]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":true}},
@@ -24,7 +24,7 @@
24
24
  {"name":"genexus_layout","description":"Layout/WebForm: get_tree, set_property, find_controls, inspect_surface, scan_mutators. Properties: genexus_properties; structure: genexus_structure get_visual.","inputSchema":{"type":"object","properties":{"action":{"type":"string","enum":["get_tree","set_property","find_controls","set_properties","inspect_surface","get_preview","scan_mutators","rename_printblock","add_printblock","delete_printblock","list_controls","design_system"]},"name":{"type":"string"},"control":{"type":"string"},"propertyName":{"type":"string"},"value":{"type":"string"},"query":{"type":"string"},"changes":{"type":"array","items":{"type":"object","properties":{"control":{"type":"string"},"propertyName":{"type":"string"},"value":{"type":"string"}},"required":["control","propertyName","value"]}},"limit":{"type":"integer"},"currentName":{"type":"string"},"newName":{"type":"string"},"printBlockName":{"type":"string"},"height":{"type":"integer"},"kb":{"type":"string","description":"KB alias."}},"required":["action"],"examples":[{"action":"get_tree","name":"WPMain"},{"action":"find_controls","name":"WPMain","query":"Button"}]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false}},
25
25
  {"name":"genexus_doc","description":"Use to generate structured docs (wiki, sequence diagrams, health reports). Don't use for programmatic analysis (see genexus_analyze) or source (see genexus_read).","inputSchema":{"type":"object","properties":{"action":{"type":"string","enum":["wiki","visualize","health"]},"target":{"type":"string","description":"Object or domain name."}},"required":["action"],"examples":[{"action":"wiki","target":"Customer"},{"action":"health"}]},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},
26
26
  {"name":"genexus_search_source","description":"Regex/semantic search across Procedure/DataProvider/WebPanel/Transaction source. Requires the KB index to be Ready β€” check genexus_lifecycle action=status if results look empty.","inputSchema":{"type":"object","properties":{"callee":{"type":"string","description":"Method/function name (qualified or unqualified)."},"argMatches":{"type":"object","description":"Positional arg index to expected literal text."},"pattern":{"type":"string"},"typeFilter":{"type":"string"},"objectName":{"type":"string","description":"Restrict the scan to these exact object name(s), comma-separated. Makes a search inside one known object O(object) not O(KB), and bypasses the type whitelist."},"startIndex":{"type":"integer","description":"Legacy object-boundary resume. For a page that ended inside an object, prefer the opaque cursor returned in nextCursor."},"cursor":{"type":"string","description":"Opaque continuation token from nextCursor; preferred when a result page ended inside an object. Do not edit."},"timeoutMs":{"type":"integer","description":"Wall-clock budget (default 30000). Raise it to scan more objects per call."},"scope":{"type":"array","items":{"type":"string","enum":["source","rules","conditions","events","webForm","layout"]},"description":"Parts to scan with line-numbered context. Default [source]. webForm/layout scans the WebPanel/Transaction visual XML (control names, captions, classes, bindings)."},"fields":{"type":"array","items":{"type":"string","enum":["source","caption","description","parmNames","webForm"]},"description":"Whole-field metadata match (returns matchedValue, no line context). Default [source]. Prefer scope=[webForm] for line-context WebForm hits; use fields=[webForm] only for a coarse contains-match."},"maxResults":{"type":"integer"},"caseSensitive":{"type":"boolean"},"includeComments":{"type":"boolean"},"inline_read_top":{"type":"integer","description":"0-3. Inline reads of top N distinct objects in response."},"kb":{"type":"string","description":"KB alias."}},"examples":[{"pattern":"ControlType=\"Radio\""},{"pattern":"for each","typeFilter":"Procedure","maxResults":20}]},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},
27
- {"name":"genexus_kb","description":"Manage open KBs, startup config, and the active GeneXus environment. list/open/close/set_default operate on WorkerPool; set_default selects the implicit target for calls that omit kb (an explicit kb always wins for parallel or cross-KB work); set_startup/get_startup mirror IDE 'Set As Startup Object'; get_environment reads the SDK selection; set_environment selects an environment through the SDK task used by the IDE.","inputSchema":{"type":"object","properties":{"action":{"type":"string","enum":["list","open","close","set_default","set_startup","get_startup","get_environment","set_environment"]},"alias":{"type":"string","description":"KB alias. open: auto from path basename if omitted."},"path":{"type":"string","description":"Absolute KB path (required for open if alias not in config)."},"name":{"type":"string","description":"Object name (required for set_startup)."},"environment":{"type":"string","description":"Environment name/folder (required for set_environment), e.g. development."}},"required":["action"],"examples":[{"action":"open","path":"C:\\KBs\\MyKb"},{"action":"list"},{"action":"get_environment"},{"action":"set_environment","environment":"development"}]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false}},
27
+ {"name":"genexus_kb","description":"Manage open KBs, startup config, and GeneXus environments. list/open/close/set_default operate on WorkerPool; set_default selects the implicit target for calls that omit kb (an explicit kb always wins for parallel or cross-KB work); set_startup/get_startup mirror IDE 'Set As Startup Object'; list_environments lists all configured environments; get_environment reads the active environment; set_environment selects an environment.","inputSchema":{"type":"object","properties":{"action":{"type":"string","enum":["list","open","close","set_default","set_startup","get_startup","list_environments","get_environment","set_environment"]},"alias":{"type":"string","description":"KB alias. open: auto from path basename if omitted."},"path":{"type":"string","description":"Absolute KB path (required for open if alias not in config)."},"name":{"type":"string","description":"Object name (required for set_startup)."},"environment":{"type":"string","description":"Environment name/folder (required for set_environment), e.g. development."}},"required":["action"],"examples":[{"action":"open","path":"C:\\KBs\\MyKb"},{"action":"list"},{"action":"list_environments"},{"action":"get_environment"},{"action":"set_environment","environment":"development"}]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false}},
28
28
  {"name":"genexus_navigation","description":"View navigation report (IDE Right-click β†’ View Navigation). latest=true returns cached report from .gx/navigation-cache; else runs fresh.","inputSchema":{"type":"object","required":["action","name"],"properties":{"action":{"type":"string","enum":["view"]},"name":{"type":"string"},"latest":{"type":"boolean","default":false,"description":"Return the most recent cached navigation if any; otherwise run fresh."},"kb":{"type":"string","description":"KB alias."}},"examples":[{"action":"view","name":"Customer"},{"action":"view","name":"Customer","latest":true}]},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},
29
29
  {"name":"genexus_api","description":"Introspect REST endpoints exposed by HTTP procedures. action=list|describe|snapshot|diff_baseline. Returns endpoints + detects breaking changes vs a saved baseline.","inputSchema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["list","describe","snapshot","diff_baseline"]},"target":{"type":"string","description":"Procedure name (action=describe)."},"name":{"type":"string","description":"Baseline name (action=snapshot)."},"baseline":{"type":"string","description":"Baseline name under .gx/api-baselines/ or absolute path (action=diff_baseline)."},"pathPrefix":{"type":"string","description":"Folder prefix filter (action=list), e.g. 'Root Module/API/'."},"kb":{"type":"string","description":"KB alias."}},"examples":[{"action":"list"},{"action":"describe","target":"ApiCustomerGet"}]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false}},
30
30
  {"name":"genexus_apply_pattern","description":"Apply a pattern (e.g. WorkWithPlus) to a parent (IDE Right-click β†’ Apply Pattern). Transaction=family-gen; WebPanel/WebComponent/SDPanel=direct-attach (pass settings.template). mode=diagnose (or dryRun=true) returns findings without mutating. Working on Smart Devices? β†’ resources/read uri=genexus://kb/skills/sd-panel-mobile for Main object semantics. mode=actions manages typed grid actions and Action Groups, persists and re-reads PatternInstance, supports dryRun diff, and never adds security permissions automatically.","inputSchema":{"type":"object","required":["name","pattern"],"properties":{"name":{"type":"string","description":"Target KBObject name."},"pattern":{"type":"string","description":"Pattern key ('WorkWithPlus') or GUID."},"mode":{"type":"string","enum":["apply","diagnose","actions"],"description":"apply (default) mutates; diagnose returns reasons without applying. actions exposes typed WWP action-group editing."},"action":{"type":"string","enum":["list_actions","add_grid_action","update_action","move_action","remove_action"],"description":"mode=actions operation."},"group":{"type":"string"},"actionName":{"type":"string"},"newGroup":{"type":"string"},"position":{"type":"integer","minimum":0},"procedure":{"type":"string"},"selection":{"type":"string","enum":["current","multiple"]},"enabledWhen":{"type":"string"},"visibleWhen":{"type":"string"},"icon":{"type":"string"},"description":{"type":"string"},"confirmation":{"type":"string"},"confirmTitle":{"type":"string"},"settings":{"type":"object","description":"Pattern-instance settings tree."},"reapply":{"type":"boolean","default":false,"description":"Re-run on existing instance."},"validate":{"type":"boolean","default":false,"description":"After apply, build the generated host. Adds validation block (60-180s). Catches binding errors."},"dryRun":{"type":"boolean","default":false,"description":"Alias for mode=diagnose. When true, returns pattern findings without mutating. With mode=actions it previews the typed action diff."},"kb":{"type":"string","description":"KB alias."}},"examples":[{"name":"Customer","pattern":"WorkWithPlus"},{"name":"Customer","pattern":"WorkWithPlus","mode":"diagnose"},{"name":"Customer","pattern":"WorkWithPlus","mode":"actions","action":"add_grid_action","group":"Actions","actionName":"Approve","procedure":"ApproveCustomer","dryRun":true}]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false}},
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);