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 +16 -1
- package/cli/commands/axi.js +24 -5
- package/cli/index.js +68 -25
- package/cli/lib/config.js +25 -12
- package/cli/lib/stdio-diagnostics.js +67 -0
- package/cli/lib/update-check.js +21 -2
- package/cli/run.js +10 -1
- package/cli/run.test.js +225 -9
- package/docs/llm_cli_mcp_playbook.md +16 -0
- package/package.json +5 -3
- package/publish/GxMcp.Gateway.deps.json +2 -2
- package/publish/GxMcp.Gateway.dll +0 -0
- package/publish/GxMcp.Gateway.exe +0 -0
- package/publish/config.json +10 -10
- package/publish/tool_definitions.json +2 -2
- package/publish/worker/GxMcp.Worker.exe +0 -0
- package/scripts/test-one.js +18 -0
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 | β
|
|
|
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
|
|
package/cli/commands/axi.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
366
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
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.
|
|
391
|
-
if (signal) {
|
|
392
|
-
|
|
393
|
-
return;
|
|
434
|
+
child.once('close', (code, signal) => {
|
|
435
|
+
if (signal || code !== 0) {
|
|
436
|
+
recordStdioFailure({ gatewayExePath, exitCode: code, signal });
|
|
394
437
|
}
|
|
395
|
-
|
|
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
|
-
|
|
453
|
-
|
|
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,
|
|
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
|
+
};
|
package/cli/lib/update-check.js
CHANGED
|
@@ -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
|
|
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
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
650
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
69
|
+
"GxMcp.Gateway/2.50.0": {
|
|
70
70
|
"type": "project",
|
|
71
71
|
"serviceable": false,
|
|
72
72
|
"sha512": ""
|
|
Binary file
|
|
Binary file
|
package/publish/config.json
CHANGED
|
@@ -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
|
-
"
|
|
4
|
-
"
|
|
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
|
|
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);
|