genexus-mcp 3.5.3 → 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 +128 -22
- package/cli/lib/client-adapters.js +8 -2
- package/cli/lib/config.js +131 -21
- package/cli/run.test.js +232 -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 +6 -6
- package/publish/gxmcp-manifest.json +10 -10
- package/publish/gxmcp-sbom.json +4 -4
- package/publish/tool_definitions.json +10 -10
- 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/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
|
@@ -59,7 +59,9 @@ function validateClientIds(ids) {
|
|
|
59
59
|
|
|
60
60
|
function isSupportedCatalogMajor(catalog, major) {
|
|
61
61
|
if (!major) return true;
|
|
62
|
-
|
|
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));
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
function parseFieldSelection(raw) {
|
|
@@ -176,6 +178,37 @@ function buildStatusData(cwd) {
|
|
|
176
178
|
return { ready, configFound, gatewayExeFound, kbLooksValid, configPath, gatewayExePath, kbPath, gxPath, configSource };
|
|
177
179
|
}
|
|
178
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
|
+
|
|
179
212
|
async function spawnGatewayProbe({ env = process.env, spawnHoldMs, timeoutMs, label, successDetail }) {
|
|
180
213
|
const gatewayExePath = getGatewayExePath();
|
|
181
214
|
|
|
@@ -209,17 +242,25 @@ async function spawnGatewayProbe({ env = process.env, spawnHoldMs, timeoutMs, la
|
|
|
209
242
|
});
|
|
210
243
|
|
|
211
244
|
child.once('spawn', () => {
|
|
212
|
-
setTimeout(() => {
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
}
|
|
215
252
|
}, spawnHoldMs);
|
|
216
253
|
});
|
|
217
254
|
|
|
218
|
-
setTimeout(() => {
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
+
});
|
|
223
264
|
}, timeoutMs);
|
|
224
265
|
} catch (err) {
|
|
225
266
|
finish({ status: 'fail', detail: `${label} threw: ${err.message}` });
|
|
@@ -562,6 +603,50 @@ function buildInProcessBuildAssemblyLoadCheck(gxPath) {
|
|
|
562
603
|
};
|
|
563
604
|
}
|
|
564
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
|
+
|
|
565
650
|
function redactConfig(cfg) {
|
|
566
651
|
// Replace absolute paths with `<redacted:hash8>` so the structure is preserved
|
|
567
652
|
// but filesystem layout, usernames, and KB names are not leaked. Hash is stable
|
|
@@ -710,7 +795,7 @@ async function handleDoctor(options, ctx) {
|
|
|
710
795
|
const kbPath = data.kbPath;
|
|
711
796
|
const gxPath = data.gxPath;
|
|
712
797
|
const kbExists = !!(kbPath && fs.existsSync(kbPath));
|
|
713
|
-
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'))));
|
|
714
799
|
const kbSdkCompatibility = kbExists && gxExeExists
|
|
715
800
|
? compareGeneXusKbAndInstallation(kbPath, gxPath)
|
|
716
801
|
: null;
|
|
@@ -731,15 +816,18 @@ async function handleDoctor(options, ctx) {
|
|
|
731
816
|
compatibilityDetail = `${unsupportedCompatibilityMajor} is not supported by this MCP distribution. Supported majors: ${catalog.supportedMajors.map((entry) => entry.major).join(', ')}.`;
|
|
732
817
|
} else if (kbSdkCompatibility.status === 'mismatch') {
|
|
733
818
|
compatibilityStatus = 'fail';
|
|
734
|
-
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.`;
|
|
735
820
|
} else if (kbSdkCompatibility.status === 'match') {
|
|
736
821
|
compatibilityStatus = 'pass';
|
|
737
|
-
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})` : ''}.`;
|
|
738
823
|
} else {
|
|
739
824
|
compatibilityDetail = `KB/SDK major compatibility could not be verified (KB: ${kbSdkCompatibility.kb.source}; SDK: ${kbSdkCompatibility.gx.source}).`;
|
|
740
825
|
}
|
|
741
826
|
}
|
|
742
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
|
+
|
|
743
831
|
const riskyZone = isPathLikelyAppLockerBlocked(gatewayExePath);
|
|
744
832
|
const clientCrossCheck = buildClientExeCrossCheck(gatewayExePath);
|
|
745
833
|
|
|
@@ -767,7 +855,7 @@ async function handleDoctor(options, ctx) {
|
|
|
767
855
|
// Same logic for the GeneXus install: missing genexus.exe at a configured path
|
|
768
856
|
// guarantees a worker crash on first MCP call. Promote from warn to fail so init
|
|
769
857
|
// exits non-zero and the caller (install.ps1, AI client) actually sees the problem.
|
|
770
|
-
{ 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.') },
|
|
771
859
|
{
|
|
772
860
|
id: 'kb_sdk_compatibility',
|
|
773
861
|
status: compatibilityStatus,
|
|
@@ -784,6 +872,13 @@ async function handleDoctor(options, ctx) {
|
|
|
784
872
|
const inProcessLoad = buildInProcessBuildAssemblyLoadCheck(gxPath);
|
|
785
873
|
checks.push({ id: 'in_process_build_assembly_load', status: inProcessLoad.status, detail: inProcessLoad.detail });
|
|
786
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
|
+
|
|
787
882
|
// Client registration summary — one line answering "are my AI agents wired up?".
|
|
788
883
|
const clientRows = clientsStatus();
|
|
789
884
|
const installedRows = clientRows.filter((r) => r.installed);
|
|
@@ -1344,6 +1439,7 @@ function buildClientLauncherHelp(patchResult) {
|
|
|
1344
1439
|
}
|
|
1345
1440
|
if (patched.length > 0 && process.platform === 'win32' && !process.env.GENEXUS_MCP_GATEWAY_EXE) {
|
|
1346
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`).');
|
|
1347
1443
|
}
|
|
1348
1444
|
return help;
|
|
1349
1445
|
}
|
|
@@ -1939,6 +2035,7 @@ async function probeWorkerStartup({ configPath, observeMs = 2500 }) {
|
|
|
1939
2035
|
};
|
|
1940
2036
|
|
|
1941
2037
|
let child;
|
|
2038
|
+
let stopping = false;
|
|
1942
2039
|
try {
|
|
1943
2040
|
child = spawn(gatewayExePath, [], {
|
|
1944
2041
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -1957,6 +2054,9 @@ async function probeWorkerStartup({ configPath, observeMs = 2500 }) {
|
|
|
1957
2054
|
});
|
|
1958
2055
|
|
|
1959
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;
|
|
1960
2060
|
if (code === 0) {
|
|
1961
2061
|
finish({ status: 'pass', detail: 'Worker smoke: gateway exited cleanly during observation window.' });
|
|
1962
2062
|
} else {
|
|
@@ -1970,10 +2070,15 @@ async function probeWorkerStartup({ configPath, observeMs = 2500 }) {
|
|
|
1970
2070
|
}
|
|
1971
2071
|
});
|
|
1972
2072
|
|
|
1973
|
-
setTimeout(() => {
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
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
|
+
}
|
|
1977
2082
|
}, observeMs);
|
|
1978
2083
|
});
|
|
1979
2084
|
}
|
|
@@ -2150,11 +2255,12 @@ async function handleClients(subcommand, options, ctx) {
|
|
|
2150
2255
|
if (invalidLaunchers.length > 0) {
|
|
2151
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}` : ''}`);
|
|
2152
2257
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
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}` : ''}`);
|
|
2158
2264
|
}
|
|
2159
2265
|
const semanticValidCount = rows.filter((r) => r.launcherSemanticState === 'valid').length;
|
|
2160
2266
|
const semanticInvalidCount = rows.filter((r) => r.launcherSemanticState === 'invalid').length;
|
|
@@ -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;
|
package/cli/lib/config.js
CHANGED
|
@@ -88,9 +88,12 @@ function getGeneXusVersionCatalog() {
|
|
|
88
88
|
const catalog = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
89
89
|
if (!catalog || !catalog.primaryMajor || !Array.isArray(catalog.supportedMajors)) continue;
|
|
90
90
|
const entries = catalog.supportedMajors.filter((entry) => entry && /^\d+$/.test(String(entry.major)));
|
|
91
|
+
const legacyEntries = Array.isArray(catalog.legacyMajors)
|
|
92
|
+
? catalog.legacyMajors.filter((entry) => entry && /^(?:10\.[1-3]|\d+)$/.test(String(entry.major)))
|
|
93
|
+
: [];
|
|
91
94
|
const primary = entries.find((entry) => String(entry.major) === String(catalog.primaryMajor));
|
|
92
95
|
if (entries.length > 0 && primary?.defaultInstallPath) {
|
|
93
|
-
return { ...catalog, supportedMajors: entries };
|
|
96
|
+
return { ...catalog, supportedMajors: entries, legacyMajors: legacyEntries };
|
|
94
97
|
}
|
|
95
98
|
} catch {
|
|
96
99
|
}
|
|
@@ -101,13 +104,19 @@ function getGeneXusVersionCatalog() {
|
|
|
101
104
|
{ major: '17', displayName: 'GeneXus 17', defaultInstallPath: 'C:\\Program Files (x86)\\GeneXus\\GeneXus17Trial' },
|
|
102
105
|
{ major: '18', displayName: 'GeneXus 18', defaultInstallPath: 'C:\\Program Files (x86)\\GeneXus\\GeneXus18' }
|
|
103
106
|
],
|
|
107
|
+
legacyMajors: [
|
|
108
|
+
{ major: '10.3', displayName: 'GeneXus Evolution 3', defaultInstallPath: 'C:\\Program Files (x86)\\GeneXus\\GeneXusXEv3' },
|
|
109
|
+
{ major: '9', displayName: 'GeneXus 9.0', defaultInstallPath: 'C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 9.0' },
|
|
110
|
+
{ major: '8', displayName: 'GeneXus 8.0', defaultInstallPath: 'C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 8.0' }
|
|
111
|
+
],
|
|
104
112
|
source: 'built-in-fallback'
|
|
105
113
|
};
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
function getGeneXusCatalogEntries(preferredMajor = null) {
|
|
109
117
|
const catalog = getGeneXusVersionCatalog();
|
|
110
|
-
|
|
118
|
+
const all = [...catalog.supportedMajors, ...(catalog.legacyMajors || [])];
|
|
119
|
+
return all.sort((left, right) => {
|
|
111
120
|
const leftPreferred = preferredMajor !== null && String(left.major) === String(preferredMajor);
|
|
112
121
|
const rightPreferred = preferredMajor !== null && String(right.major) === String(preferredMajor);
|
|
113
122
|
if (leftPreferred !== rightPreferred) return Number(rightPreferred) - Number(leftPreferred);
|
|
@@ -708,6 +717,12 @@ function resolveOpenCodeConfigPath(xdgConfig) {
|
|
|
708
717
|
return path.join(xdgConfig, 'opencode', 'opencode.json');
|
|
709
718
|
}
|
|
710
719
|
|
|
720
|
+
function alternateOpenCodeConfigPath(filePath) {
|
|
721
|
+
return path.extname(filePath).toLowerCase() === '.jsonc'
|
|
722
|
+
? filePath.slice(0, -1)
|
|
723
|
+
: filePath.replace(/\.json$/i, '.jsonc');
|
|
724
|
+
}
|
|
725
|
+
|
|
711
726
|
// VS Code stores its user profile (and native MCP mcp.json) in a per-platform
|
|
712
727
|
// location. `variant` is 'Code' (stable) or 'Code - Insiders'.
|
|
713
728
|
function vscodeUserDir(variant, { appData, macAppSupport, xdgConfig }) {
|
|
@@ -807,6 +822,7 @@ function getClientConfigTargets() {
|
|
|
807
822
|
name: 'OpenCode (CLI)',
|
|
808
823
|
format: 'opencode',
|
|
809
824
|
path: resolveOpenCodeConfigPath(xdgConfig),
|
|
825
|
+
alternatePaths: [alternateOpenCodeConfigPath(resolveOpenCodeConfigPath(xdgConfig))],
|
|
810
826
|
installMarkers: [
|
|
811
827
|
path.join(xdgConfig, 'opencode'),
|
|
812
828
|
path.join(home, '.local', 'share', 'opencode')
|
|
@@ -826,6 +842,7 @@ function getClientConfigTargets() {
|
|
|
826
842
|
name: 'OpenCode Desktop',
|
|
827
843
|
format: 'opencode',
|
|
828
844
|
path: resolveOpenCodeConfigPath(xdgConfig),
|
|
845
|
+
alternatePaths: [alternateOpenCodeConfigPath(resolveOpenCodeConfigPath(xdgConfig))],
|
|
829
846
|
detectByMarkerOnly: true,
|
|
830
847
|
installMarkers: [
|
|
831
848
|
path.join(localAppData, 'Programs', '@opencode-aidesktop'),
|
|
@@ -937,21 +954,27 @@ function clientCommandHealth(entry, client = null, { fs: fileSystem = fs } = {})
|
|
|
937
954
|
reason: null,
|
|
938
955
|
structuralState: 'indeterminate',
|
|
939
956
|
semanticState: 'unknown',
|
|
940
|
-
semanticReason: null
|
|
957
|
+
semanticReason: null,
|
|
958
|
+
pathDrift: false,
|
|
959
|
+
pathDriftReason: null
|
|
941
960
|
};
|
|
942
961
|
const invalid = (reason, structuralState = 'present') => ({
|
|
943
962
|
stale: true,
|
|
944
963
|
reason,
|
|
945
964
|
structuralState,
|
|
946
965
|
semanticState: 'invalid',
|
|
947
|
-
semanticReason: reason
|
|
966
|
+
semanticReason: reason,
|
|
967
|
+
pathDrift: false,
|
|
968
|
+
pathDriftReason: null
|
|
948
969
|
});
|
|
949
970
|
const valid = (structuralState = 'present') => ({
|
|
950
971
|
stale: false,
|
|
951
972
|
reason: null,
|
|
952
973
|
structuralState,
|
|
953
974
|
semanticState: 'valid',
|
|
954
|
-
semanticReason: null
|
|
975
|
+
semanticReason: null,
|
|
976
|
+
pathDrift: false,
|
|
977
|
+
pathDriftReason: null
|
|
955
978
|
});
|
|
956
979
|
const missingLauncherReason = 'configured launcher does not exist on disk';
|
|
957
980
|
|
|
@@ -961,7 +984,9 @@ function clientCommandHealth(entry, client = null, { fs: fileSystem = fs } = {})
|
|
|
961
984
|
reason: null,
|
|
962
985
|
structuralState: 'not-registered',
|
|
963
986
|
semanticState: 'not-applicable',
|
|
964
|
-
semanticReason: null
|
|
987
|
+
semanticReason: null,
|
|
988
|
+
pathDrift: false,
|
|
989
|
+
pathDriftReason: null
|
|
965
990
|
};
|
|
966
991
|
}
|
|
967
992
|
if (typeof entry !== 'object') {
|
|
@@ -973,7 +998,9 @@ function clientCommandHealth(entry, client = null, { fs: fileSystem = fs } = {})
|
|
|
973
998
|
reason: null,
|
|
974
999
|
structuralState: 'not-applicable',
|
|
975
1000
|
semanticState: 'not-applicable',
|
|
976
|
-
semanticReason: null
|
|
1001
|
+
semanticReason: null,
|
|
1002
|
+
pathDrift: false,
|
|
1003
|
+
pathDriftReason: null
|
|
977
1004
|
};
|
|
978
1005
|
}
|
|
979
1006
|
|
|
@@ -1004,13 +1031,21 @@ function clientCommandHealth(entry, client = null, { fs: fileSystem = fs } = {})
|
|
|
1004
1031
|
};
|
|
1005
1032
|
}
|
|
1006
1033
|
if (!exists) return invalid(missingLauncherReason, 'missing');
|
|
1034
|
+
// A gateway exe that exists at a different absolute path is a *different,
|
|
1035
|
+
// working* install — a local checkout's publish/ copy, a fixed-path install,
|
|
1036
|
+
// or another package cache — not a broken registration. Reporting it as
|
|
1037
|
+
// `stale` made `npx genexus-mcp clients` tell a checkout operator to
|
|
1038
|
+
// re-register, which rewrote the harness away from the checkout gateway
|
|
1039
|
+
// (Issue #210). The drift is still surfaced (as `pathDrift`, never `stale`)
|
|
1040
|
+
// and doctor's `client_config_sync` check keeps the "not this CLI's packaged
|
|
1041
|
+
// gateway" warning with its own remediation.
|
|
1007
1042
|
if (client && client.preferDirectGateway) {
|
|
1008
1043
|
const currentPackageGateway = getGatewayExePath();
|
|
1009
|
-
if (
|
|
1044
|
+
if (fileSystem.existsSync(currentPackageGateway) && normalizeExePath(command) !== normalizeExePath(currentPackageGateway)) {
|
|
1010
1045
|
return {
|
|
1011
1046
|
...valid('present'),
|
|
1012
|
-
|
|
1013
|
-
|
|
1047
|
+
pathDrift: true,
|
|
1048
|
+
pathDriftReason: `configured gateway differs from this CLI's gateway (${currentPackageGateway})`
|
|
1014
1049
|
};
|
|
1015
1050
|
}
|
|
1016
1051
|
}
|
|
@@ -1131,13 +1166,16 @@ function clientsStatus(opts = {}) {
|
|
|
1131
1166
|
serverName,
|
|
1132
1167
|
isThirdParty: Boolean(isThirdParty),
|
|
1133
1168
|
writeSupported: client.writeSupported !== false,
|
|
1134
|
-
configPath: client.path,
|
|
1169
|
+
configPath: entry && entry.configPath ? entry.configPath : client.path,
|
|
1170
|
+
configPaths: entry && entry.configPath ? [entry.configPath] : [client.path],
|
|
1135
1171
|
command: entry && entry.command ? entry.command : null,
|
|
1136
1172
|
args: entry && Array.isArray(entry.args) ? entry.args : [],
|
|
1137
1173
|
url: entry && entry.url ? entry.url : null,
|
|
1138
1174
|
launcherStructuralState: health.structuralState,
|
|
1139
1175
|
launcherSemanticState: health.semanticState,
|
|
1140
1176
|
launcherSemanticReason: health.semanticReason,
|
|
1177
|
+
launcherPathDrift: health.pathDrift,
|
|
1178
|
+
launcherPathDriftReason: health.pathDriftReason,
|
|
1141
1179
|
commandStale: health.stale,
|
|
1142
1180
|
commandStaleReason: health.reason || (isThirdParty ? 'configured as third-party / HTTP MCP server (e.g. official GeneXus MCP)' : null),
|
|
1143
1181
|
detectedAt: det.markerHit || (det.hasConfig ? client.path : null),
|
|
@@ -1426,7 +1464,7 @@ function getOpenCodeMcpContainer(cfgObj) {
|
|
|
1426
1464
|
};
|
|
1427
1465
|
}
|
|
1428
1466
|
|
|
1429
|
-
function applyOpenCodeJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false, globalConfig = false, fs: fileSystem = fs } = {}) {
|
|
1467
|
+
function applyOpenCodeJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false, globalConfig = false, alternatePaths = [], fs: fileSystem = fs } = {}) {
|
|
1430
1468
|
const parsed = fileSystem.existsSync(filePath) ? readJsonFileSafe(filePath, fileSystem) : {};
|
|
1431
1469
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
1432
1470
|
const cfgObj = parsed || {};
|
|
@@ -1470,10 +1508,20 @@ function applyOpenCodeJson(filePath, launcher, targetConfigPath, { serverName =
|
|
|
1470
1508
|
}
|
|
1471
1509
|
}
|
|
1472
1510
|
writeClientJson(filePath, cfgObj, fileSystem);
|
|
1511
|
+
for (const alternatePath of alternatePaths || []) {
|
|
1512
|
+
if (!fileSystem.existsSync(alternatePath)) continue;
|
|
1513
|
+
applyOpenCodeJson(alternatePath, launcher, targetConfigPath, {
|
|
1514
|
+
serverName,
|
|
1515
|
+
force,
|
|
1516
|
+
globalConfig,
|
|
1517
|
+
alternatePaths: [],
|
|
1518
|
+
fs: fileSystem
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1473
1521
|
}
|
|
1474
1522
|
|
|
1475
|
-
function removeOpenCodeJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME, fs: fileSystem = fs } = {}) {
|
|
1476
|
-
const parsed = readJsonFileSafe(filePath);
|
|
1523
|
+
function removeOpenCodeJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME, alternatePaths = [], fs: fileSystem = fs } = {}) {
|
|
1524
|
+
const parsed = readJsonFileSafe(filePath, fileSystem);
|
|
1477
1525
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
1478
1526
|
const cfgObj = parsed || {};
|
|
1479
1527
|
if (!cfgObj.mcp || typeof cfgObj.mcp !== 'object') return false;
|
|
@@ -1503,9 +1551,17 @@ function removeOpenCodeJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME, fs
|
|
|
1503
1551
|
}
|
|
1504
1552
|
}
|
|
1505
1553
|
}
|
|
1506
|
-
if (
|
|
1507
|
-
|
|
1508
|
-
|
|
1554
|
+
if (removedAny) writeClientJson(filePath, cfgObj, fileSystem);
|
|
1555
|
+
let removedAlternate = false;
|
|
1556
|
+
for (const alternatePath of alternatePaths || []) {
|
|
1557
|
+
if (!fileSystem.existsSync(alternatePath)) continue;
|
|
1558
|
+
removedAlternate = removeOpenCodeJson(alternatePath, {
|
|
1559
|
+
serverName,
|
|
1560
|
+
alternatePaths: [],
|
|
1561
|
+
fs: fileSystem
|
|
1562
|
+
}) || removedAlternate;
|
|
1563
|
+
}
|
|
1564
|
+
return removedAny || removedAlternate;
|
|
1509
1565
|
}
|
|
1510
1566
|
|
|
1511
1567
|
function extractCodexTomlEntry(content, serverName = DEFAULT_MCP_SERVER_NAME) {
|
|
@@ -1650,8 +1706,49 @@ function normalizeExePath(p) {
|
|
|
1650
1706
|
return s;
|
|
1651
1707
|
}
|
|
1652
1708
|
|
|
1709
|
+
function readOpenCodeCommandEntry(filePath, serverName, fileSystem) {
|
|
1710
|
+
if (!fileSystem.existsSync(filePath)) return null;
|
|
1711
|
+
const parsed = readJsonFileSafe(filePath, fileSystem);
|
|
1712
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
1713
|
+
let entry = parsed.mcp?.servers?.[serverName] || parsed.mcp?.[serverName];
|
|
1714
|
+
if (!entry && serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
1715
|
+
const legacyGx = parsed.mcp?.servers?.genexus || parsed.mcp?.genexus;
|
|
1716
|
+
if (legacyGx && !isThirdPartyMcpEntry(legacyGx)) entry = legacyGx;
|
|
1717
|
+
else entry = parsed.mcp?.servers?.genexus18 || parsed.mcp?.genexus18;
|
|
1718
|
+
}
|
|
1719
|
+
if (!entry) return null;
|
|
1720
|
+
if (Array.isArray(entry.command) && entry.command.length > 0) {
|
|
1721
|
+
return {
|
|
1722
|
+
command: entry.command[0],
|
|
1723
|
+
args: entry.command.slice(1),
|
|
1724
|
+
url: entry.url || null,
|
|
1725
|
+
type: entry.type || null,
|
|
1726
|
+
environment: entry.environment || null,
|
|
1727
|
+
raw: entry,
|
|
1728
|
+
configPath: filePath
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
return {
|
|
1732
|
+
command: null,
|
|
1733
|
+
args: [],
|
|
1734
|
+
url: entry.url || null,
|
|
1735
|
+
type: entry.type || null,
|
|
1736
|
+
environment: entry.environment || null,
|
|
1737
|
+
raw: entry,
|
|
1738
|
+
configPath: filePath
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1653
1742
|
function readClientCommandEntry(client, serverName = DEFAULT_MCP_SERVER_NAME, { fs: fileSystem = fs } = {}) {
|
|
1654
1743
|
if (client.writeSupported === false) return null;
|
|
1744
|
+
if (client.format === 'opencode') {
|
|
1745
|
+
const paths = [client.path, ...(client.alternatePaths || [])];
|
|
1746
|
+
for (const configPath of paths) {
|
|
1747
|
+
const entry = readOpenCodeCommandEntry(configPath, serverName, fileSystem);
|
|
1748
|
+
if (entry) return entry;
|
|
1749
|
+
}
|
|
1750
|
+
return null;
|
|
1751
|
+
}
|
|
1655
1752
|
if (!fileSystem.existsSync(client.path)) return null;
|
|
1656
1753
|
try {
|
|
1657
1754
|
if (client.format === 'mcpServers') {
|
|
@@ -1752,7 +1849,7 @@ function getLocalAppDataCacheDir() {
|
|
|
1752
1849
|
}
|
|
1753
1850
|
|
|
1754
1851
|
function getGeneXusMajor(version) {
|
|
1755
|
-
const match = String(version || '').match(/^\s*(
|
|
1852
|
+
const match = String(version || '').match(/^\s*(10\.[1-3](?!\d)|\d+)/);
|
|
1756
1853
|
return match ? match[1] : null;
|
|
1757
1854
|
}
|
|
1758
1855
|
|
|
@@ -1809,7 +1906,10 @@ function readGeneXusInstallationIdentity(gxPath, options = {}) {
|
|
|
1809
1906
|
const readExecutableVersion = typeof options.readExecutableVersion === 'function'
|
|
1810
1907
|
? options.readExecutableVersion
|
|
1811
1908
|
: readExecutableProductVersion;
|
|
1812
|
-
|
|
1909
|
+
let executableVersion = readExecutableVersion(path.join(gxPath, 'GeneXus.exe'));
|
|
1910
|
+
if (!executableVersion) {
|
|
1911
|
+
executableVersion = readExecutableVersion(path.join(gxPath, 'gx.exe'));
|
|
1912
|
+
}
|
|
1813
1913
|
if (executableVersion) {
|
|
1814
1914
|
return {
|
|
1815
1915
|
version: executableVersion,
|
|
@@ -1895,14 +1995,24 @@ function readGeneXusKbIdentity(kbPath) {
|
|
|
1895
1995
|
if (!kbPath) return { version: null, major: null, source: 'unavailable', reason: 'missing-kb-path' };
|
|
1896
1996
|
|
|
1897
1997
|
let gxwFiles;
|
|
1998
|
+
let gxiFiles = [];
|
|
1898
1999
|
try {
|
|
1899
|
-
|
|
2000
|
+
const allFiles = fs.readdirSync(kbPath);
|
|
2001
|
+
gxwFiles = allFiles
|
|
1900
2002
|
.filter((fileName) => fileName.toLowerCase().endsWith('.gxw'))
|
|
1901
2003
|
.map((fileName) => path.join(kbPath, fileName));
|
|
2004
|
+
gxiFiles = allFiles
|
|
2005
|
+
.filter((fileName) => fileName.toLowerCase().endsWith('.gxi'))
|
|
2006
|
+
.map((fileName) => path.join(kbPath, fileName));
|
|
1902
2007
|
} catch {
|
|
1903
2008
|
return { version: null, major: null, source: 'unavailable', reason: 'unreadable-kb-path' };
|
|
1904
2009
|
}
|
|
1905
|
-
if (gxwFiles.length === 0)
|
|
2010
|
+
if (gxwFiles.length === 0) {
|
|
2011
|
+
if (gxiFiles.length > 0) {
|
|
2012
|
+
return { version: '9.0', major: '9', source: 'gxi-classic', reason: null };
|
|
2013
|
+
}
|
|
2014
|
+
return { version: null, major: null, source: 'unavailable', reason: 'no-gxw' };
|
|
2015
|
+
}
|
|
1906
2016
|
if (gxwFiles.length > 1) return { version: null, major: null, source: 'unavailable', reason: 'multiple-gxw' };
|
|
1907
2017
|
|
|
1908
2018
|
try {
|