genexus-mcp 2.49.1 → 2.49.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +4 -4
- 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.49.
|
|
3
|
+
"version": "2.49.2",
|
|
4
4
|
"mcpName": "io.github.lennix1337/genexus",
|
|
5
5
|
"description": "GeneXus 18 MCP server — read, edit, and analyze GeneXus knowledge base objects (transactions, web panels, procedures, SDTs) directly from Claude, Cursor, and other AI agents over the Model Context Protocol.",
|
|
6
6
|
"keywords": [
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
33
|
"test": "node --test cli/run.test.js",
|
|
34
|
-
"lint": "eslint cli scripts eslint.config.js",
|
|
34
|
+
"lint": "eslint cli scripts eslint.config.js --max-warnings=0",
|
|
35
|
+
"test:one": "node scripts/test-one.js",
|
|
35
36
|
"prepack": "node scripts/clean-package.js",
|
|
36
37
|
"postinstall": "node scripts/verify-install.js"
|
|
37
38
|
},
|
|
@@ -42,7 +43,8 @@
|
|
|
42
43
|
"cli",
|
|
43
44
|
"publish",
|
|
44
45
|
"docs/llm_cli_mcp_playbook.md",
|
|
45
|
-
"scripts/verify-install.js"
|
|
46
|
+
"scripts/verify-install.js",
|
|
47
|
+
"scripts/test-one.js"
|
|
46
48
|
],
|
|
47
49
|
"os": [
|
|
48
50
|
"win32"
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"targets": {
|
|
8
8
|
".NETCoreApp,Version=v8.0": {},
|
|
9
9
|
".NETCoreApp,Version=v8.0/win-x64": {
|
|
10
|
-
"GxMcp.Gateway/2.49.
|
|
10
|
+
"GxMcp.Gateway/2.49.2": {
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"Newtonsoft.Json": "13.0.3",
|
|
13
13
|
"System.Management": "10.0.5",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
"libraries": {
|
|
69
|
-
"GxMcp.Gateway/2.49.
|
|
69
|
+
"GxMcp.Gateway/2.49.2": {
|
|
70
70
|
"type": "project",
|
|
71
71
|
"serviceable": false,
|
|
72
72
|
"sha512": ""
|
|
Binary file
|
|
Binary file
|
package/publish/config.json
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"InstallationPath": "C:\\\\Program Files (x86)\\\\GeneXus\\\\GeneXus18",
|
|
4
4
|
"WorkerExecutable": "C:\\Projetos\\Genexus18MCP\\publish\\\\worker\\\\GxMcp.Worker.exe"
|
|
5
5
|
},
|
|
6
|
+
"Logging": {
|
|
7
|
+
"Level": "Debug",
|
|
8
|
+
"Path": "logs"
|
|
9
|
+
},
|
|
6
10
|
"Environment": {
|
|
7
11
|
"KBPath": "C:\\\\KBs\\\\YourKB"
|
|
8
12
|
},
|
|
9
|
-
"Logging": {
|
|
10
|
-
"Path": "logs",
|
|
11
|
-
"Level": "Debug"
|
|
12
|
-
},
|
|
13
13
|
"Server": {
|
|
14
14
|
"HttpPort": 5000,
|
|
15
15
|
"McpStdio": true
|
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawnSync } = require('node:child_process');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const pattern = process.argv.slice(2).join(' ').trim();
|
|
6
|
+
if (!pattern) {
|
|
7
|
+
process.stderr.write('Usage: npm run test:one -- "test name pattern"\n');
|
|
8
|
+
process.exit(2);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const result = spawnSync(process.execPath, [
|
|
12
|
+
'--test',
|
|
13
|
+
'--test-name-pattern',
|
|
14
|
+
pattern,
|
|
15
|
+
path.join(__dirname, '..', 'cli', 'run.test.js')
|
|
16
|
+
], { stdio: 'inherit' });
|
|
17
|
+
|
|
18
|
+
process.exit(result.status === null ? 1 : result.status);
|