genexus-mcp 3.5.2 → 3.6.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 +8 -1
- package/cli/commands/axi.js +166 -23
- package/cli/index.js +42 -4
- package/cli/lib/client-adapters.js +8 -2
- package/cli/lib/config.js +131 -21
- package/cli/run.js +4 -3
- package/cli/run.test.js +310 -12
- package/config/gx-versions.json +57 -0
- package/docs/llm_cli_mcp_playbook.md +23 -0
- package/package.json +3 -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/gx-versions.json +57 -0
- package/publish/config.json +8 -8
- package/publish/gxmcp-manifest.json +10 -15
- package/publish/gxmcp-sbom.json +4 -4
- package/publish/tool_definitions.json +12 -12
- package/publish/worker/GxMcp.Worker.exe +0 -0
- package/publish/worker/config/gx-versions.json +57 -0
- package/publish/worker/gx-versions.json +57 -0
- package/publish/worker/sdk-compatibility.json +1 -1
- package/scripts/verify-install.js +2 -1
- package/publish/nexus-ide.vsix +0 -0
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ needed. Existing tool names, arguments, and MCP client configuration formats do
|
|
|
57
57
|
not change.
|
|
58
58
|
|
|
59
59
|
<!-- BEGIN GENERATED: gx-compatibility -->
|
|
60
|
-
Supported SDK majors: **GeneXus 17, GeneXus 18**.
|
|
60
|
+
Supported SDK majors: **GeneXus 16, GeneXus 17, GeneXus 18**.
|
|
61
61
|
Primary SDK: **GeneXus 18**.
|
|
62
62
|
Source of truth: `config/gx-versions.json`.
|
|
63
63
|
<!-- END GENERATED: gx-compatibility -->
|
|
@@ -67,6 +67,13 @@ catalog only after compiling the Worker with that SDK and passing the focused
|
|
|
67
67
|
tests plus a live KB smoke. This prevents the server from claiming compatibility
|
|
68
68
|
based only on a version string.
|
|
69
69
|
|
|
70
|
+
### Legacy GeneXus compatibility (GX8 to GX15)
|
|
71
|
+
|
|
72
|
+
The server also includes best-effort dynamic compatibility for legacy installations:
|
|
73
|
+
- **GeneXus Evolution 1 (10.1), Evolution 2 (10.2), Evolution 3 (10.3), and GeneXus 15**: Driven via runtime reflection (`dotnet-reflection`), dynamically adapting to missing types or structural differences (such as module-less KBs without `QualifiedName`).
|
|
74
|
+
- **GeneXus 8.0 and GeneXus 9.0**: Driven via classic Win32 COM automation (`com-gxpublic`), late-binding to `GXPublic.GXPublic` on an STA thread to open, inspect, and read objects from classic `.gxi` Knowledge Bases.
|
|
75
|
+
- **Graceful degradation**: Modern tools that require features introduced in newer GeneXus versions (such as `genexus_api`, `genexus_gam`, or `genexus_module`) return structured `UNSUPPORTED_IN_GENEXUS_VERSION` errors indicating the required minimum version rather than failing ungracefully.
|
|
76
|
+
|
|
70
77
|
---
|
|
71
78
|
|
|
72
79
|
## What you can do with it
|
package/cli/commands/axi.js
CHANGED
|
@@ -36,6 +36,7 @@ const {
|
|
|
36
36
|
DEFAULT_MCP_SERVER_NAME
|
|
37
37
|
} = require('../lib/config');
|
|
38
38
|
const { getStdioErrorLogPath } = require('../lib/stdio-diagnostics');
|
|
39
|
+
const { getPackageVersion } = require('../lib/update-check');
|
|
39
40
|
|
|
40
41
|
function resolveClientIds(options) {
|
|
41
42
|
if (!options || !options.clients) return null;
|
|
@@ -58,7 +59,9 @@ function validateClientIds(ids) {
|
|
|
58
59
|
|
|
59
60
|
function isSupportedCatalogMajor(catalog, major) {
|
|
60
61
|
if (!major) return true;
|
|
61
|
-
|
|
62
|
+
const inSupported = catalog.supportedMajors && catalog.supportedMajors.some((entry) => String(entry.major) === String(major));
|
|
63
|
+
if (inSupported) return true;
|
|
64
|
+
return Array.isArray(catalog.legacyMajors) && catalog.legacyMajors.some((entry) => String(entry.major) === String(major));
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
function parseFieldSelection(raw) {
|
|
@@ -175,6 +178,37 @@ function buildStatusData(cwd) {
|
|
|
175
178
|
return { ready, configFound, gatewayExeFound, kbLooksValid, configPath, gatewayExePath, kbPath, gxPath, configSource };
|
|
176
179
|
}
|
|
177
180
|
|
|
181
|
+
const PROBE_EXIT_GRACE_MS = 2000;
|
|
182
|
+
|
|
183
|
+
// Stop a probe child and wait for it to actually exit. On Windows child.kill()
|
|
184
|
+
// only signals the direct process and returns before the OS releases the
|
|
185
|
+
// executable image, so reporting success right after kill() left the exe file
|
|
186
|
+
// handle open (Issue #211: the shared gateway stub in cli/run.test.js made the
|
|
187
|
+
// suite teardown fail with EPERM while every assertion passed). Resolves true
|
|
188
|
+
// once the child exited, false when it did not within the bounded grace.
|
|
189
|
+
function stopProbeChild(child, graceMs = PROBE_EXIT_GRACE_MS) {
|
|
190
|
+
return new Promise((resolve) => {
|
|
191
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) {
|
|
192
|
+
resolve(true);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
let settled = false;
|
|
196
|
+
const finish = (exited) => {
|
|
197
|
+
if (settled) return;
|
|
198
|
+
settled = true;
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
resolve(exited);
|
|
201
|
+
};
|
|
202
|
+
const timer = setTimeout(() => finish(false), graceMs);
|
|
203
|
+
child.once('exit', () => finish(true));
|
|
204
|
+
try {
|
|
205
|
+
child.kill();
|
|
206
|
+
} catch {
|
|
207
|
+
finish(true);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
178
212
|
async function spawnGatewayProbe({ env = process.env, spawnHoldMs, timeoutMs, label, successDetail }) {
|
|
179
213
|
const gatewayExePath = getGatewayExePath();
|
|
180
214
|
|
|
@@ -208,17 +242,25 @@ async function spawnGatewayProbe({ env = process.env, spawnHoldMs, timeoutMs, la
|
|
|
208
242
|
});
|
|
209
243
|
|
|
210
244
|
child.once('spawn', () => {
|
|
211
|
-
setTimeout(() => {
|
|
212
|
-
|
|
213
|
-
|
|
245
|
+
setTimeout(async () => {
|
|
246
|
+
const exited = await stopProbeChild(child);
|
|
247
|
+
if (exited) {
|
|
248
|
+
finish({ status: 'pass', detail: successDetail });
|
|
249
|
+
} else {
|
|
250
|
+
finish({ status: 'warn', detail: `${label}: process did not exit after the stop signal; it may still hold the gateway exe.` });
|
|
251
|
+
}
|
|
214
252
|
}, spawnHoldMs);
|
|
215
253
|
});
|
|
216
254
|
|
|
217
|
-
setTimeout(() => {
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
255
|
+
setTimeout(async () => {
|
|
256
|
+
if (done) return;
|
|
257
|
+
const exited = await stopProbeChild(child);
|
|
258
|
+
finish({
|
|
259
|
+
status: 'warn',
|
|
260
|
+
detail: exited
|
|
261
|
+
? `${label} timed out; process was force-stopped.`
|
|
262
|
+
: `${label} timed out and did not exit after the stop signal; it may still hold the gateway exe.`
|
|
263
|
+
});
|
|
222
264
|
}, timeoutMs);
|
|
223
265
|
} catch (err) {
|
|
224
266
|
finish({ status: 'fail', detail: `${label} threw: ${err.message}` });
|
|
@@ -561,6 +603,50 @@ function buildInProcessBuildAssemblyLoadCheck(gxPath) {
|
|
|
561
603
|
};
|
|
562
604
|
}
|
|
563
605
|
|
|
606
|
+
function buildGxPublicComCheck(gxMajor, gxPath) {
|
|
607
|
+
if (gxMajor !== '8' && gxMajor !== '9') {
|
|
608
|
+
return { id: 'gxpublic_com_registration', status: 'not_applicable', detail: 'Not a GeneXus 8.0/9.0 installation.' };
|
|
609
|
+
}
|
|
610
|
+
const child_process = require('child_process');
|
|
611
|
+
let isRegistered = false;
|
|
612
|
+
try {
|
|
613
|
+
const res = child_process.spawnSync('reg', ['query', 'HKCR\\GXPublic.GXPublic'], { encoding: 'utf8', timeout: 2000 });
|
|
614
|
+
isRegistered = res.status === 0;
|
|
615
|
+
} catch { }
|
|
616
|
+
|
|
617
|
+
if (isRegistered) {
|
|
618
|
+
return { id: 'gxpublic_com_registration', status: 'pass', detail: 'GXPublic COM automation server (GXPublic.GXPublic) is registered.' };
|
|
619
|
+
}
|
|
620
|
+
const dllHint = gxPath ? path.join(gxPath, 'GxPublic.dll') : 'GxPublic.dll';
|
|
621
|
+
return {
|
|
622
|
+
id: 'gxpublic_com_registration',
|
|
623
|
+
status: 'warn',
|
|
624
|
+
detail: `GXPublic COM component is not registered. Run 'regsvr32 "${dllHint}"' to enable GeneXus ${gxMajor}.0 COM automation.`
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function buildLegacyIdeLockCheck(gxMajor) {
|
|
629
|
+
if (gxMajor !== '8' && gxMajor !== '9' && gxMajor !== '10.1' && gxMajor !== '10.2') {
|
|
630
|
+
return { id: 'legacy_ide_lock', status: 'not_applicable', detail: 'Lock check not applicable for modern GeneXus versions.' };
|
|
631
|
+
}
|
|
632
|
+
const child_process = require('child_process');
|
|
633
|
+
let ideRunning = false;
|
|
634
|
+
try {
|
|
635
|
+
const res = child_process.spawnSync('tasklist', ['/FI', 'IMAGENAME eq gx.exe', '/NH'], { encoding: 'utf8', timeout: 2000 });
|
|
636
|
+
if (res.stdout && res.stdout.toLowerCase().includes('gx.exe')) {
|
|
637
|
+
ideRunning = true;
|
|
638
|
+
}
|
|
639
|
+
} catch { }
|
|
640
|
+
if (ideRunning) {
|
|
641
|
+
return {
|
|
642
|
+
id: 'legacy_ide_lock',
|
|
643
|
+
status: 'warn',
|
|
644
|
+
detail: 'GeneXus IDE (gx.exe) process is currently running. Legacy engines hold exclusive file locks on the KB.'
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
return { id: 'legacy_ide_lock', status: 'pass', detail: 'No conflicting GeneXus IDE process (gx.exe) detected.' };
|
|
648
|
+
}
|
|
649
|
+
|
|
564
650
|
function redactConfig(cfg) {
|
|
565
651
|
// Replace absolute paths with `<redacted:hash8>` so the structure is preserved
|
|
566
652
|
// but filesystem layout, usernames, and KB names are not leaked. Hash is stable
|
|
@@ -709,7 +795,7 @@ async function handleDoctor(options, ctx) {
|
|
|
709
795
|
const kbPath = data.kbPath;
|
|
710
796
|
const gxPath = data.gxPath;
|
|
711
797
|
const kbExists = !!(kbPath && fs.existsSync(kbPath));
|
|
712
|
-
const gxExeExists = !!(gxPath && fs.existsSync(path.join(gxPath, 'genexus.exe')));
|
|
798
|
+
const gxExeExists = !!(gxPath && (fs.existsSync(path.join(gxPath, 'genexus.exe')) || fs.existsSync(path.join(gxPath, 'gx.exe'))));
|
|
713
799
|
const kbSdkCompatibility = kbExists && gxExeExists
|
|
714
800
|
? compareGeneXusKbAndInstallation(kbPath, gxPath)
|
|
715
801
|
: null;
|
|
@@ -730,15 +816,18 @@ async function handleDoctor(options, ctx) {
|
|
|
730
816
|
compatibilityDetail = `${unsupportedCompatibilityMajor} is not supported by this MCP distribution. Supported majors: ${catalog.supportedMajors.map((entry) => entry.major).join(', ')}.`;
|
|
731
817
|
} else if (kbSdkCompatibility.status === 'mismatch') {
|
|
732
818
|
compatibilityStatus = 'fail';
|
|
733
|
-
compatibilityDetail = `KB major ${kbSdkCompatibility.kb.major} does not match GeneXus SDK major ${kbSdkCompatibility.gx.major}. Re-run init with the matching --gx path.`;
|
|
819
|
+
compatibilityDetail = `KB major ${kbSdkCompatibility.kb.major} does not match GeneXus SDK major ${kbSdkCompatibility.gx.major}${kbSdkCompatibility.gx.version ? ` (${kbSdkCompatibility.gx.version})` : ''}. Re-run init with the matching --gx path.`;
|
|
734
820
|
} else if (kbSdkCompatibility.status === 'match') {
|
|
735
821
|
compatibilityStatus = 'pass';
|
|
736
|
-
compatibilityDetail = `KB major ${kbSdkCompatibility.kb.major} matches GeneXus SDK major ${kbSdkCompatibility.gx.major}.`;
|
|
822
|
+
compatibilityDetail = `KB major ${kbSdkCompatibility.kb.major} matches GeneXus SDK major ${kbSdkCompatibility.gx.major}${kbSdkCompatibility.gx.version ? ` (${kbSdkCompatibility.gx.version})` : ''}.`;
|
|
737
823
|
} else {
|
|
738
824
|
compatibilityDetail = `KB/SDK major compatibility could not be verified (KB: ${kbSdkCompatibility.kb.source}; SDK: ${kbSdkCompatibility.gx.source}).`;
|
|
739
825
|
}
|
|
740
826
|
}
|
|
741
827
|
|
|
828
|
+
const gxIdent = gxPath ? readGeneXusInstallationIdentity(gxPath) : null;
|
|
829
|
+
const gxVersionLabel = gxIdent && gxIdent.version ? ` (GeneXus ${gxIdent.major || ''} version ${gxIdent.version})` : (gxIdent && gxIdent.major ? ` (GeneXus ${gxIdent.major})` : '');
|
|
830
|
+
|
|
742
831
|
const riskyZone = isPathLikelyAppLockerBlocked(gatewayExePath);
|
|
743
832
|
const clientCrossCheck = buildClientExeCrossCheck(gatewayExePath);
|
|
744
833
|
|
|
@@ -766,7 +855,7 @@ async function handleDoctor(options, ctx) {
|
|
|
766
855
|
// Same logic for the GeneXus install: missing genexus.exe at a configured path
|
|
767
856
|
// guarantees a worker crash on first MCP call. Promote from warn to fail so init
|
|
768
857
|
// exits non-zero and the caller (install.ps1, AI client) actually sees the problem.
|
|
769
|
-
{ id: 'gx_installation', status: gxExeExists ? 'pass' : (gxPath ? 'fail' : 'warn'), detail: gxExeExists ?
|
|
858
|
+
{ id: 'gx_installation', status: gxExeExists ? 'pass' : (gxPath ? 'fail' : 'warn'), detail: gxExeExists ? `GeneXus installation has ${gxPath && fs.existsSync(path.join(gxPath, 'gx.exe')) && !fs.existsSync(path.join(gxPath, 'genexus.exe')) ? 'gx.exe' : 'genexus.exe'}${gxVersionLabel}.` : (gxPath ? `Configured GeneXus installation is missing executable at: ${gxPath}` : 'No GeneXus installation path is configured.') },
|
|
770
859
|
{
|
|
771
860
|
id: 'kb_sdk_compatibility',
|
|
772
861
|
status: compatibilityStatus,
|
|
@@ -783,6 +872,13 @@ async function handleDoctor(options, ctx) {
|
|
|
783
872
|
const inProcessLoad = buildInProcessBuildAssemblyLoadCheck(gxPath);
|
|
784
873
|
checks.push({ id: 'in_process_build_assembly_load', status: inProcessLoad.status, detail: inProcessLoad.detail });
|
|
785
874
|
|
|
875
|
+
// Legacy GeneXus checks (GX 8.0, 9.0, Ev1, Ev2)
|
|
876
|
+
const gxMajor = (gxIdent && gxIdent.major) || (kbSdkCompatibility && kbSdkCompatibility.gx && kbSdkCompatibility.gx.major) || null;
|
|
877
|
+
const comCheck = buildGxPublicComCheck(gxMajor, gxPath);
|
|
878
|
+
checks.push({ id: 'gxpublic_com_registration', status: comCheck.status, detail: comCheck.detail });
|
|
879
|
+
const ideLockCheck = buildLegacyIdeLockCheck(gxMajor);
|
|
880
|
+
checks.push({ id: 'legacy_ide_lock', status: ideLockCheck.status, detail: ideLockCheck.detail });
|
|
881
|
+
|
|
786
882
|
// Client registration summary — one line answering "are my AI agents wired up?".
|
|
787
883
|
const clientRows = clientsStatus();
|
|
788
884
|
const installedRows = clientRows.filter((r) => r.installed);
|
|
@@ -1343,6 +1439,7 @@ function buildClientLauncherHelp(patchResult) {
|
|
|
1343
1439
|
}
|
|
1344
1440
|
if (patched.length > 0 && process.platform === 'win32' && !process.env.GENEXUS_MCP_GATEWAY_EXE) {
|
|
1345
1441
|
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.');
|
|
1442
|
+
help.push('Working from a local checkout? Register the clients with the checkout gateway from the repo root instead: `$env:GENEXUS_MCP_GATEWAY_EXE="<repoRoot>\\publish\\GxMcp.Gateway.exe"; node cli\\run.js clients add --clients <ids>` (or re-run `.\\install.ps1`). Avoid `npx @latest clients add` there, because it rewrites the registration to this npm package\'s launcher; validate with the same checkout CLI (`node cli\\run.js clients` / `doctor`).');
|
|
1346
1443
|
}
|
|
1347
1444
|
return help;
|
|
1348
1445
|
}
|
|
@@ -1938,6 +2035,7 @@ async function probeWorkerStartup({ configPath, observeMs = 2500 }) {
|
|
|
1938
2035
|
};
|
|
1939
2036
|
|
|
1940
2037
|
let child;
|
|
2038
|
+
let stopping = false;
|
|
1941
2039
|
try {
|
|
1942
2040
|
child = spawn(gatewayExePath, [], {
|
|
1943
2041
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -1956,6 +2054,9 @@ async function probeWorkerStartup({ configPath, observeMs = 2500 }) {
|
|
|
1956
2054
|
});
|
|
1957
2055
|
|
|
1958
2056
|
child.once('exit', (code) => {
|
|
2057
|
+
// The observation timeout stops the gateway itself; that deliberate
|
|
2058
|
+
// signal is reported by the timeout branch, not as a crash.
|
|
2059
|
+
if (stopping) return;
|
|
1959
2060
|
if (code === 0) {
|
|
1960
2061
|
finish({ status: 'pass', detail: 'Worker smoke: gateway exited cleanly during observation window.' });
|
|
1961
2062
|
} else {
|
|
@@ -1969,10 +2070,15 @@ async function probeWorkerStartup({ configPath, observeMs = 2500 }) {
|
|
|
1969
2070
|
}
|
|
1970
2071
|
});
|
|
1971
2072
|
|
|
1972
|
-
setTimeout(() => {
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2073
|
+
setTimeout(async () => {
|
|
2074
|
+
stopping = true;
|
|
2075
|
+
const exited = await stopProbeChild(child);
|
|
2076
|
+
if (exited) {
|
|
2077
|
+
// Still alive after observeMs → worker bootstrapped without crashing.
|
|
2078
|
+
finish({ status: 'pass', detail: `Worker smoke: gateway stayed alive for ${observeMs}ms with KB and GX configured.` });
|
|
2079
|
+
} else {
|
|
2080
|
+
finish({ status: 'warn', detail: 'Worker smoke: gateway did not exit after the stop signal; it may still hold the gateway exe.' });
|
|
2081
|
+
}
|
|
1976
2082
|
}, observeMs);
|
|
1977
2083
|
});
|
|
1978
2084
|
}
|
|
@@ -2149,11 +2255,12 @@ async function handleClients(subcommand, options, ctx) {
|
|
|
2149
2255
|
if (invalidLaunchers.length > 0) {
|
|
2150
2256
|
help.push(`These clients have a known-invalid MCP launcher (check command and args) — re-register: genexus-mcp clients add --clients ${invalidLaunchers.map((r) => r.id).join(',')}${serverName !== DEFAULT_MCP_SERVER_NAME ? ` --server-name ${serverName}` : ''}`);
|
|
2151
2257
|
}
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2258
|
+
// A launcher that exists but is not this CLI's gateway is a different working
|
|
2259
|
+
// install (checkout, fixed-path, other package cache), so it is reported as
|
|
2260
|
+
// informational drift instead of a stale registration that needs repair.
|
|
2261
|
+
const driftedLaunchers = rows.filter((r) => r.launcherPathDrift && !r.commandStale);
|
|
2262
|
+
if (driftedLaunchers.length > 0) {
|
|
2263
|
+
help.push(`Note: ${driftedLaunchers.map((r) => r.name).join(', ')} point at a gateway that is not this CLI's (informational — the launcher exists and is valid). Re-register with this CLI only if you want them to track it: genexus-mcp clients add --clients ${driftedLaunchers.map((r) => r.id).join(',')}${serverName !== DEFAULT_MCP_SERVER_NAME ? ` --server-name ${serverName}` : ''}`);
|
|
2157
2264
|
}
|
|
2158
2265
|
const semanticValidCount = rows.filter((r) => r.launcherSemanticState === 'valid').length;
|
|
2159
2266
|
const semanticInvalidCount = rows.filter((r) => r.launcherSemanticState === 'invalid').length;
|
|
@@ -2483,6 +2590,13 @@ function commandHelpMap() {
|
|
|
2483
2590
|
'genexus-mcp update --channel next # check the @next dist-tag'
|
|
2484
2591
|
]
|
|
2485
2592
|
},
|
|
2593
|
+
version: {
|
|
2594
|
+
usage: 'genexus-mcp --version | -v | version [--format toon|json|text]',
|
|
2595
|
+
examples: [
|
|
2596
|
+
'genexus-mcp --version # prints the bare version (scripts/CI)',
|
|
2597
|
+
'genexus-mcp -v --format json # axi-cli/1 envelope with ok.version'
|
|
2598
|
+
]
|
|
2599
|
+
},
|
|
2486
2600
|
layout: {
|
|
2487
2601
|
usage: 'genexus-mcp layout status [--title "GeneXus"] [--format ...] OR genexus-mcp layout run --action <focus|activate-layout|activate-tab|send-keys|type-text|click> [--tab "Layout"] [--keys "..."] [--text "..."] [--x N --y N] [--title "..."] [--format ...] OR genexus-mcp layout inspect [--tab "Layout"] [--limit N] [--full] [--title "..."] [--format ...]',
|
|
2488
2602
|
examples: ['genexus-mcp layout status --format json', 'genexus-mcp layout run --action activate-tab --tab "Layout" --format json', 'genexus-mcp layout inspect --tab Layout --format json']
|
|
@@ -2518,6 +2632,34 @@ async function handleHome(_options, ctx) {
|
|
|
2518
2632
|
};
|
|
2519
2633
|
}
|
|
2520
2634
|
|
|
2635
|
+
// Issue #207: `genexus-mcp --version` / `-v` / `version` used to fall through to the
|
|
2636
|
+
// gateway passthrough (which never answers that token) and exit 0 with no output — a
|
|
2637
|
+
// silent false positive for install checks. The package version is read from the same
|
|
2638
|
+
// helper the update flow uses.
|
|
2639
|
+
// `deps` is an injection seam so the unreadable-package.json path is testable.
|
|
2640
|
+
async function handleVersion(_options, ctx, deps = {}) {
|
|
2641
|
+
const readVersion = deps.getPackageVersion || getPackageVersion;
|
|
2642
|
+
const version = readVersion();
|
|
2643
|
+
if (!version) {
|
|
2644
|
+
return {
|
|
2645
|
+
exitCode: ctx.EXIT_CODES.ERROR,
|
|
2646
|
+
envelope: operationalErrorEnvelope(
|
|
2647
|
+
'Could not read the genexus-mcp package version (package.json missing or unreadable).',
|
|
2648
|
+
ctx.EXIT_CODES.ERROR,
|
|
2649
|
+
['Reinstall the CLI (`genexus-mcp update`) so package.json can be read.'],
|
|
2650
|
+
'version_unavailable'
|
|
2651
|
+
)
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
return {
|
|
2655
|
+
exitCode: ctx.EXIT_CODES.OK,
|
|
2656
|
+
envelope: {
|
|
2657
|
+
ok: { version },
|
|
2658
|
+
help: []
|
|
2659
|
+
}
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2521
2663
|
async function handleHelp(targetCommand, ctx) {
|
|
2522
2664
|
const binPath = collapseHome(process.argv[1] || process.execPath);
|
|
2523
2665
|
const map = commandHelpMap();
|
|
@@ -2547,7 +2689,7 @@ async function handleHelp(targetCommand, ctx) {
|
|
|
2547
2689
|
bin: binPath,
|
|
2548
2690
|
command: 'genexus-mcp',
|
|
2549
2691
|
description: 'GeneXus MCP launcher and AXI-oriented utility CLI',
|
|
2550
|
-
commands: ['home', 'axi home', 'status', 'doctor', 'tools list', 'config show', 'config create', 'layout status', 'layout run', 'layout inspect', 'init', 'whoami', 'uninstall', 'kb list', 'kb add', 'kb remove', 'kb switch', 'llm help', 'update', 'help'],
|
|
2692
|
+
commands: ['home', 'axi home', 'status', 'doctor', 'tools list', 'config show', 'config create', 'layout status', 'layout run', 'layout inspect', 'init', 'whoami', 'uninstall', 'kb list', 'kb add', 'kb remove', 'kb switch', 'llm help', 'update', 'version', 'help'],
|
|
2551
2693
|
defaults: { format: 'toon', limit: 100 }
|
|
2552
2694
|
},
|
|
2553
2695
|
help: [
|
|
@@ -2660,6 +2802,7 @@ module.exports = {
|
|
|
2660
2802
|
handleLlmHelp,
|
|
2661
2803
|
handleLayout,
|
|
2662
2804
|
handleHelp,
|
|
2805
|
+
handleVersion,
|
|
2663
2806
|
usageEnvelope,
|
|
2664
2807
|
operationalErrorEnvelope,
|
|
2665
2808
|
resolveMcpSmokeTarget,
|
package/cli/index.js
CHANGED
|
@@ -26,6 +26,7 @@ const {
|
|
|
26
26
|
handleLlmHelp,
|
|
27
27
|
handleLayout,
|
|
28
28
|
handleHelp,
|
|
29
|
+
handleVersion,
|
|
29
30
|
usageEnvelope,
|
|
30
31
|
commandHelpMap
|
|
31
32
|
} = require('./commands/axi');
|
|
@@ -63,7 +64,19 @@ const GLOBAL_DEFAULTS = {
|
|
|
63
64
|
help: false
|
|
64
65
|
};
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
// Single source of truth for command routing: cli/run.js imports both sets so the
|
|
68
|
+
// AXI-vs-passthrough decision (stdout vs stderr for unhandled errors) cannot drift
|
|
69
|
+
// from the parser again — issue #207 was caused by two hand-synced copies.
|
|
70
|
+
const KNOWN_COMMANDS = new Set(['status', 'doctor', 'tools', 'config', 'init', 'setup', 'whoami', 'uninstall', 'kb', 'clients', 'help', 'home', 'axi', 'llm', 'layout', 'update', 'version']);
|
|
71
|
+
|
|
72
|
+
// Version query aliases. `-v` deliberately is NOT an alias of `--help`/`-h`: those
|
|
73
|
+
// return immediately, while the version aliases are command tokens so that remaining
|
|
74
|
+
// flags (`-v --format json`) are still parsed and honored (issue #207).
|
|
75
|
+
const VERSION_ALIASES = new Set(['version', '-v', '--version']);
|
|
76
|
+
|
|
77
|
+
function isKnownCommandToken(token) {
|
|
78
|
+
return KNOWN_COMMANDS.has(token) || VERSION_ALIASES.has(token);
|
|
79
|
+
}
|
|
67
80
|
|
|
68
81
|
function parseArgs(argv) {
|
|
69
82
|
const result = {
|
|
@@ -79,7 +92,8 @@ function parseArgs(argv) {
|
|
|
79
92
|
if (tokens.length === 0) return result;
|
|
80
93
|
|
|
81
94
|
const first = tokens[0];
|
|
82
|
-
|
|
95
|
+
const versionIntent = VERSION_ALIASES.has(first);
|
|
96
|
+
if (!versionIntent && !KNOWN_COMMANDS.has(first) && !first.startsWith('--')) {
|
|
83
97
|
return result;
|
|
84
98
|
}
|
|
85
99
|
|
|
@@ -89,7 +103,13 @@ function parseArgs(argv) {
|
|
|
89
103
|
return result;
|
|
90
104
|
}
|
|
91
105
|
|
|
92
|
-
if (
|
|
106
|
+
if (versionIntent) {
|
|
107
|
+
// Treat the alias as a consumed command token and keep parsing the remaining
|
|
108
|
+
// flags, so `version --format json`, `-v --format json` and `--version --format json`
|
|
109
|
+
// all reach the format validation instead of falling through to passthrough.
|
|
110
|
+
result.command = 'version';
|
|
111
|
+
tokens.shift();
|
|
112
|
+
} else if (KNOWN_COMMANDS.has(first)) {
|
|
93
113
|
result.command = first === 'setup' ? 'init' : first;
|
|
94
114
|
tokens.shift();
|
|
95
115
|
}
|
|
@@ -534,7 +554,9 @@ function resolveMetaCommand(parsed, targetHelp) {
|
|
|
534
554
|
async function main(argv) {
|
|
535
555
|
const parsed = parseArgs(argv);
|
|
536
556
|
|
|
537
|
-
|
|
557
|
+
// `version` is a quiet query: no update-check banner (it would corrupt the raw
|
|
558
|
+
// version string scripts read) and no launcher config side effects.
|
|
559
|
+
if (parsed.command !== 'update' && parsed.command !== 'version') {
|
|
538
560
|
startBackgroundUpdateCheck({ quiet: parsed.options.quiet });
|
|
539
561
|
}
|
|
540
562
|
|
|
@@ -568,6 +590,19 @@ async function main(argv) {
|
|
|
568
590
|
return helpResult.exitCode;
|
|
569
591
|
}
|
|
570
592
|
|
|
593
|
+
if (parsed.command === 'version') {
|
|
594
|
+
const versionResult = await handleVersion(parsed.options, ctx);
|
|
595
|
+
// Default formats print the bare version so `genexus-mcp --version` is usable
|
|
596
|
+
// from scripts/CI; only --format json opts into the axi-cli/1 envelope.
|
|
597
|
+
if (versionResult.exitCode === EXIT_CODES.OK
|
|
598
|
+
&& (parsed.options.format === 'toon' || parsed.options.format === 'text')) {
|
|
599
|
+
process.stdout.write(`${versionResult.envelope.ok.version}\n`);
|
|
600
|
+
} else {
|
|
601
|
+
writeStructured(process.stdout, withCommandMeta(versionResult.envelope, 'version'), parsed.options.format);
|
|
602
|
+
}
|
|
603
|
+
return versionResult.exitCode;
|
|
604
|
+
}
|
|
605
|
+
|
|
571
606
|
let result;
|
|
572
607
|
|
|
573
608
|
switch (parsed.command) {
|
|
@@ -682,6 +717,9 @@ module.exports = {
|
|
|
682
717
|
main,
|
|
683
718
|
parseArgs,
|
|
684
719
|
EXIT_CODES,
|
|
720
|
+
KNOWN_COMMANDS,
|
|
721
|
+
VERSION_ALIASES,
|
|
722
|
+
isKnownCommandToken,
|
|
685
723
|
renderOutput,
|
|
686
724
|
formatToonObject
|
|
687
725
|
};
|
|
@@ -78,10 +78,16 @@ class OpenCodeJsoncAdapter extends BaseClientAdapter {
|
|
|
78
78
|
this.config = configModule || require('./config');
|
|
79
79
|
}
|
|
80
80
|
apply(client, launcher, targetConfigPath, opts = {}) {
|
|
81
|
-
return this.config.applyOpenCodeJson(client.path, launcher, targetConfigPath,
|
|
81
|
+
return this.config.applyOpenCodeJson(client.path, launcher, targetConfigPath, {
|
|
82
|
+
...opts,
|
|
83
|
+
alternatePaths: client.alternatePaths || []
|
|
84
|
+
});
|
|
82
85
|
}
|
|
83
86
|
remove(client, opts = {}) {
|
|
84
|
-
return this.config.removeOpenCodeJson(client.path,
|
|
87
|
+
return this.config.removeOpenCodeJson(client.path, {
|
|
88
|
+
...opts,
|
|
89
|
+
alternatePaths: client.alternatePaths || []
|
|
90
|
+
});
|
|
85
91
|
}
|
|
86
92
|
read(client, serverName) {
|
|
87
93
|
if (!fs.existsSync(client.path)) return null;
|