genexus-mcp 3.6.1 → 3.7.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 +108 -9
- package/cli/commands/axi.js +311 -64
- package/cli/lib/config.js +99 -19
- package/cli/run.test.js +226 -0
- package/config/gx-versions.json +5 -3
- package/package.json +1 -1
- 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 +5 -3
- package/publish/config.json +12 -12
- package/publish/gxmcp-manifest.json +10 -10
- package/publish/gxmcp-sbom.json +4 -4
- package/publish/tool_definitions.json +4 -4
- package/publish/worker/GxMcp.Worker.exe +0 -0
- package/publish/worker/GxMcp.Worker.exe.config +12 -0
- package/publish/worker/Microsoft.Bcl.AsyncInterfaces.dll +0 -0
- package/publish/worker/Microsoft.Bcl.HashCode.dll +0 -0
- package/publish/worker/Microsoft.Extensions.DependencyInjection.Abstractions.dll +0 -0
- package/publish/worker/Microsoft.Extensions.Logging.Abstractions.dll +0 -0
- package/publish/worker/Npgsql.dll +0 -0
- package/publish/worker/System.Buffers.dll +0 -0
- package/publish/worker/System.Collections.Immutable.dll +0 -0
- package/publish/worker/System.Diagnostics.DiagnosticSource.dll +0 -0
- package/publish/worker/System.Memory.dll +0 -0
- package/publish/worker/System.Numerics.Vectors.dll +0 -0
- package/publish/worker/System.Runtime.CompilerServices.Unsafe.dll +0 -0
- package/publish/worker/System.Text.Encodings.Web.dll +0 -0
- package/publish/worker/System.Text.Json.dll +0 -0
- package/publish/worker/System.Threading.Channels.dll +0 -0
- package/publish/worker/System.Threading.Tasks.Extensions.dll +0 -0
- package/publish/worker/System.ValueTuple.dll +0 -0
- package/publish/worker/config/gx-versions.json +5 -3
- package/publish/worker/gx-versions.json +5 -3
package/cli/lib/config.js
CHANGED
|
@@ -33,6 +33,7 @@ function generateNeutralConfig(gxPath, { workerPath, gatewayMode = 'stdio-isolat
|
|
|
33
33
|
Server: {
|
|
34
34
|
HttpPort: 0,
|
|
35
35
|
McpStdio: true,
|
|
36
|
+
WorkerSharingMode: 'isolated',
|
|
36
37
|
SessionIdleTimeoutMinutes: 10,
|
|
37
38
|
WorkerIdleTimeoutMinutes: 5,
|
|
38
39
|
EmitStructuredContent: false,
|
|
@@ -70,6 +71,10 @@ function getToolDefinitionsPath() {
|
|
|
70
71
|
// 3. Fallback alongside the CLI itself (defensive — for unusual layouts).
|
|
71
72
|
path.join(__dirname, '..', '..', 'publish', 'tool_definitions.json')
|
|
72
73
|
];
|
|
74
|
+
// An explicit gateway override is usually a fixed-path or checkout install.
|
|
75
|
+
// Do not hide a missing sibling artifact with this source tree's copy: the
|
|
76
|
+
// Gateway loads the file next to its own executable at runtime.
|
|
77
|
+
if (process.env.GENEXUS_MCP_GATEWAY_EXE) return candidates[0];
|
|
73
78
|
for (const candidate of candidates) {
|
|
74
79
|
if (fs.existsSync(candidate)) return candidate;
|
|
75
80
|
}
|
|
@@ -107,14 +112,14 @@ function getGeneXusVersionCatalog() {
|
|
|
107
112
|
legacyMajors: [
|
|
108
113
|
{ major: '10.3', displayName: 'GeneXus Evolution 3', defaultInstallPath: 'C:\\Program Files (x86)\\GeneXus\\GeneXusXEv3' },
|
|
109
114
|
{ 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\\
|
|
115
|
+
{ major: '8', displayName: 'GeneXus 8.0', defaultInstallPath: 'C:\\Program Files (x86)\\ARTech\\GeneXus\\gxw80' }
|
|
111
116
|
],
|
|
112
117
|
source: 'built-in-fallback'
|
|
113
118
|
};
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
function hasGeneXusExecutable(gxPath) {
|
|
117
|
-
return Boolean(gxPath) && ['genexus.exe', 'gx.exe']
|
|
122
|
+
return Boolean(gxPath) && ['genexus.exe', 'gx.exe', 'gxw32.exe']
|
|
118
123
|
.some((name) => fs.existsSync(path.join(gxPath, name)));
|
|
119
124
|
}
|
|
120
125
|
|
|
@@ -165,6 +170,24 @@ function discoverGeneXusFromRegistry(preferredMajor = null) {
|
|
|
165
170
|
} catch {
|
|
166
171
|
}
|
|
167
172
|
}
|
|
173
|
+
// Classic ARTech installers store GX8/GX9 under Setup\\<version>
|
|
174
|
+
// and expose the actual IDE root in the Install value rather
|
|
175
|
+
// than the modern GeneXus <major>\\InstallationDirectory key.
|
|
176
|
+
for (const setupVersion of (entry.legacySetupVersions || [])) {
|
|
177
|
+
const key = `${hive}\\\\${setupVersion}`;
|
|
178
|
+
try {
|
|
179
|
+
const out = execFileSync('reg.exe', ['query', key, '/v', 'Install'], {
|
|
180
|
+
encoding: 'utf8',
|
|
181
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
182
|
+
windowsHide: true,
|
|
183
|
+
timeout: 3000
|
|
184
|
+
});
|
|
185
|
+
const installLine = out.split(String.fromCharCode(10)).map((line) => line.trim()).find((line) => /^Install\s+REG_SZ\s+/i.test(line));
|
|
186
|
+
const candidate = installLine ? installLine.replace(/^Install\s+REG_SZ\s+/i, '').trim() : null;
|
|
187
|
+
if (candidate && hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) return candidate;
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
168
191
|
for (const legacyVersion of (entry.legacyRegistryVersions || [])) {
|
|
169
192
|
const key = `${hive}\\GeneXus\\${legacyVersion}`;
|
|
170
193
|
try {
|
|
@@ -259,7 +282,7 @@ function discoverGeneXusFromPath(preferredMajor = null) {
|
|
|
259
282
|
if (process.platform !== 'win32') return null;
|
|
260
283
|
try {
|
|
261
284
|
const { execFileSync } = require('child_process');
|
|
262
|
-
for (const executable of ['genexus.exe', 'gx.exe']) {
|
|
285
|
+
for (const executable of ['genexus.exe', 'gx.exe', 'gxw32.exe']) {
|
|
263
286
|
let out;
|
|
264
287
|
try {
|
|
265
288
|
out = execFileSync('where.exe', [executable], {
|
|
@@ -361,7 +384,9 @@ function discoverKnowledgeBases(cwd, { maxResults = 25, scanDepth = 2 } = {}) {
|
|
|
361
384
|
function directoryLooksLikeKnowledgeBase(dir) {
|
|
362
385
|
try {
|
|
363
386
|
const files = fs.readdirSync(dir);
|
|
364
|
-
|
|
387
|
+
if (files.some((f) => f.toLowerCase().endsWith('.gxw') || f.toLowerCase() === 'knowledgebase.connection')) return true;
|
|
388
|
+
const classicMarkers = new Set(['data001', 'gxspc001', 'kbdata', 'attribut.dat', 'att.xpw', 'objects.dat', 'objects.idx']);
|
|
389
|
+
return files.filter((f) => classicMarkers.has(f.toLowerCase())).length >= 2;
|
|
365
390
|
} catch {
|
|
366
391
|
return false;
|
|
367
392
|
}
|
|
@@ -603,8 +628,11 @@ function migrateLegacyConfig(sourcePath, targetPath, { rejectNonMigratable = fal
|
|
|
603
628
|
|
|
604
629
|
function resolveConfigPathNoMutate(cwd) {
|
|
605
630
|
const cwdConfigPath = path.join(cwd, 'config.json');
|
|
606
|
-
|
|
607
|
-
|
|
631
|
+
// Match the Gateway: an explicit GX_CONFIG_PATH is authoritative even when
|
|
632
|
+
// the file is missing, so diagnostics do not silently inspect an unrelated
|
|
633
|
+
// cwd config while the runtime fails on the explicit path.
|
|
634
|
+
if (process.env.GX_CONFIG_PATH) {
|
|
635
|
+
return path.resolve(process.env.GX_CONFIG_PATH);
|
|
608
636
|
}
|
|
609
637
|
if (fs.existsSync(cwdConfigPath)) {
|
|
610
638
|
return cwdConfigPath;
|
|
@@ -1927,6 +1955,9 @@ function readGeneXusInstallationIdentity(gxPath, options = {}) {
|
|
|
1927
1955
|
if (!executableVersion) {
|
|
1928
1956
|
executableVersion = readExecutableVersion(path.join(gxPath, 'gx.exe'));
|
|
1929
1957
|
}
|
|
1958
|
+
if (!executableVersion) {
|
|
1959
|
+
executableVersion = readExecutableVersion(path.join(gxPath, 'gxw32.exe'));
|
|
1960
|
+
}
|
|
1930
1961
|
if (executableVersion) {
|
|
1931
1962
|
return {
|
|
1932
1963
|
version: executableVersion,
|
|
@@ -2013,6 +2044,7 @@ function readGeneXusKbIdentity(kbPath) {
|
|
|
2013
2044
|
|
|
2014
2045
|
let gxwFiles;
|
|
2015
2046
|
let gxiFiles = [];
|
|
2047
|
+
let classicMarkerCount = 0;
|
|
2016
2048
|
try {
|
|
2017
2049
|
const allFiles = fs.readdirSync(kbPath);
|
|
2018
2050
|
gxwFiles = allFiles
|
|
@@ -2021,6 +2053,8 @@ function readGeneXusKbIdentity(kbPath) {
|
|
|
2021
2053
|
gxiFiles = allFiles
|
|
2022
2054
|
.filter((fileName) => fileName.toLowerCase().endsWith('.gxi'))
|
|
2023
2055
|
.map((fileName) => path.join(kbPath, fileName));
|
|
2056
|
+
const classicMarkers = new Set(['data001', 'gxspc001', 'kbdata', 'attribut.dat', 'att.xpw', 'objects.dat', 'objects.idx']);
|
|
2057
|
+
classicMarkerCount = allFiles.filter((fileName) => classicMarkers.has(fileName.toLowerCase())).length;
|
|
2024
2058
|
} catch {
|
|
2025
2059
|
return { version: null, major: null, source: 'unavailable', reason: 'unreadable-kb-path' };
|
|
2026
2060
|
}
|
|
@@ -2028,6 +2062,9 @@ function readGeneXusKbIdentity(kbPath) {
|
|
|
2028
2062
|
if (gxiFiles.length > 0) {
|
|
2029
2063
|
return { version: '9.0', major: '9', source: 'gxi-classic', reason: null };
|
|
2030
2064
|
}
|
|
2065
|
+
if (classicMarkerCount >= 2) {
|
|
2066
|
+
return { version: null, major: null, source: 'classic-dat', reason: 'classic-generation-requires-provider' };
|
|
2067
|
+
}
|
|
2031
2068
|
return { version: null, major: null, source: 'unavailable', reason: 'no-gxw' };
|
|
2032
2069
|
}
|
|
2033
2070
|
if (gxwFiles.length > 1) return { version: null, major: null, source: 'unavailable', reason: 'multiple-gxw' };
|
|
@@ -2049,34 +2086,65 @@ function readGeneXusKbIdentity(kbPath) {
|
|
|
2049
2086
|
}
|
|
2050
2087
|
}
|
|
2051
2088
|
|
|
2052
|
-
function compareGeneXusKbAndInstallation(kbPath, gxPath) {
|
|
2089
|
+
function compareGeneXusKbAndInstallation(kbPath, gxPath, options = {}) {
|
|
2053
2090
|
const kb = readGeneXusKbIdentity(kbPath);
|
|
2054
|
-
const gx = readGeneXusInstallationIdentity(gxPath);
|
|
2091
|
+
const gx = readGeneXusInstallationIdentity(gxPath, options);
|
|
2055
2092
|
let status = 'unresolved';
|
|
2056
|
-
|
|
2093
|
+
// A classic .gxi is shared by the GX8/GX9 family and does not carry a
|
|
2094
|
+
// reliable generation marker. Do not turn the historical default of 9
|
|
2095
|
+
// into a false GX8-vs-GX9 mismatch; the provider open is the authority.
|
|
2096
|
+
if ((kb.source === 'gxi-classic' || kb.source === 'classic-dat') && (gx.major === '8' || gx.major === '9')) {
|
|
2097
|
+
status = 'unresolved';
|
|
2098
|
+
} else if (kb.major && gx.major) {
|
|
2099
|
+
status = kb.major === gx.major ? 'match' : 'mismatch';
|
|
2100
|
+
}
|
|
2057
2101
|
return { status, kb, gx };
|
|
2058
2102
|
}
|
|
2059
2103
|
|
|
2060
2104
|
function normalizeKbCatalog(raw) {
|
|
2105
|
+
const normalized = {};
|
|
2106
|
+
const entries = {};
|
|
2107
|
+
const add = (name, value) => {
|
|
2108
|
+
if (typeof name !== 'string' || !name) return;
|
|
2109
|
+
if (typeof value === 'string' && value) {
|
|
2110
|
+
normalized[name] = value;
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
if (!value || typeof value !== 'object') return;
|
|
2114
|
+
const kbPath = typeof value.path === 'string' ? value.path : value.Path;
|
|
2115
|
+
if (typeof kbPath !== 'string' || !kbPath) return;
|
|
2116
|
+
normalized[name] = kbPath;
|
|
2117
|
+
const metadata = { alias: name, path: kbPath };
|
|
2118
|
+
let hasLegacyMetadata = false;
|
|
2119
|
+
for (const [source, target] of [['driver', 'driver'], ['Driver', 'driver'], ['installationPath', 'installationPath'], ['InstallationPath', 'installationPath'], ['major', 'major'], ['Major', 'major']]) {
|
|
2120
|
+
if (typeof value[source] === 'string' && value[source]) {
|
|
2121
|
+
metadata[target] = value[source];
|
|
2122
|
+
hasLegacyMetadata = true;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
if (hasLegacyMetadata) entries[name] = metadata;
|
|
2126
|
+
};
|
|
2127
|
+
|
|
2061
2128
|
if (Array.isArray(raw)) {
|
|
2062
|
-
const normalized = {};
|
|
2063
2129
|
for (const entry of raw) {
|
|
2064
2130
|
if (!entry || typeof entry !== 'object') continue;
|
|
2065
2131
|
const name = typeof entry.alias === 'string' ? entry.alias : entry.Alias;
|
|
2066
|
-
|
|
2067
|
-
if (typeof name === 'string' && name && typeof kbPath === 'string' && kbPath) {
|
|
2068
|
-
normalized[name] = kbPath;
|
|
2069
|
-
}
|
|
2132
|
+
add(name, entry);
|
|
2070
2133
|
}
|
|
2134
|
+
Object.defineProperty(normalized, '__entries', { value: entries, enumerable: false, writable: true });
|
|
2071
2135
|
return normalized;
|
|
2072
2136
|
}
|
|
2073
2137
|
|
|
2074
|
-
|
|
2138
|
+
if (raw && typeof raw === 'object') {
|
|
2139
|
+
for (const [name, value] of Object.entries(raw)) add(name, value);
|
|
2140
|
+
}
|
|
2141
|
+
Object.defineProperty(normalized, '__entries', { value: entries, enumerable: false, writable: true });
|
|
2142
|
+
return normalized;
|
|
2075
2143
|
}
|
|
2076
2144
|
|
|
2077
|
-
function readKbCatalog(configPath) {
|
|
2078
|
-
if (!configPath) return { kbs: {}, activeKb: null, kbPath: null };
|
|
2079
|
-
const cfg = readJsonFileSafe(configPath);
|
|
2145
|
+
function readKbCatalog(configPath, configOverride = undefined) {
|
|
2146
|
+
if (!configPath && configOverride === undefined) return { kbs: {}, activeKb: null, kbPath: null };
|
|
2147
|
+
const cfg = configOverride === undefined ? readJsonFileSafe(configPath) : configOverride;
|
|
2080
2148
|
if (!cfg) return { kbs: {}, activeKb: null, kbPath: null };
|
|
2081
2149
|
const env = cfg.Environment || {};
|
|
2082
2150
|
const activeKb = typeof env.ActiveKb === 'string' && env.ActiveKb
|
|
@@ -2092,7 +2160,19 @@ function readKbCatalog(configPath) {
|
|
|
2092
2160
|
function writeKbCatalog(configPath, { kbs, activeKb, kbPath }) {
|
|
2093
2161
|
const cfg = readJsonFileSafe(configPath) || {};
|
|
2094
2162
|
cfg.Environment = cfg.Environment || {};
|
|
2095
|
-
|
|
2163
|
+
const serializedKbs = {};
|
|
2164
|
+
for (const [name, kbPathValue] of Object.entries(kbs || {})) {
|
|
2165
|
+
const metadata = kbs.__entries && kbs.__entries[name];
|
|
2166
|
+
serializedKbs[name] = metadata
|
|
2167
|
+
? {
|
|
2168
|
+
Path: kbPathValue,
|
|
2169
|
+
...(metadata.driver ? { Driver: metadata.driver } : {}),
|
|
2170
|
+
...(metadata.installationPath ? { InstallationPath: metadata.installationPath } : {}),
|
|
2171
|
+
...(metadata.major ? { Major: metadata.major } : {})
|
|
2172
|
+
}
|
|
2173
|
+
: kbPathValue;
|
|
2174
|
+
}
|
|
2175
|
+
cfg.Environment.KBs = serializedKbs;
|
|
2096
2176
|
if (activeKb) {
|
|
2097
2177
|
cfg.Environment.ActiveKb = activeKb;
|
|
2098
2178
|
cfg.Environment.DefaultKb = activeKb;
|
package/cli/run.test.js
CHANGED
|
@@ -22,8 +22,11 @@ const {
|
|
|
22
22
|
getGeneXusCatalogEntries,
|
|
23
23
|
discoverGeneXusInstallation,
|
|
24
24
|
readGeneXusInstallationIdentity,
|
|
25
|
+
directoryLooksLikeKnowledgeBase,
|
|
25
26
|
readGeneXusKbIdentity,
|
|
26
27
|
compareGeneXusKbAndInstallation,
|
|
28
|
+
readKbCatalog,
|
|
29
|
+
switchActiveKb,
|
|
27
30
|
patchClientConfig
|
|
28
31
|
} = require('./lib/config');
|
|
29
32
|
const { handleInit, resolveMcpSmokeTarget } = require('./commands/axi');
|
|
@@ -571,6 +574,23 @@ test('classic GeneXus installation identity reads gx.exe metadata', () => {
|
|
|
571
574
|
}
|
|
572
575
|
});
|
|
573
576
|
|
|
577
|
+
test('GeneXus 8 installation identity reads gxw32.exe metadata', () => {
|
|
578
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-gx8-version-'));
|
|
579
|
+
try {
|
|
580
|
+
fs.writeFileSync(path.join(tempRoot, 'gxw32.exe'), 'not-a-real-executable');
|
|
581
|
+
const identity = readGeneXusInstallationIdentity(tempRoot, {
|
|
582
|
+
readExecutableVersion: (exePath) => exePath.endsWith('gxw32.exe') ? '8.0.0.632' : null
|
|
583
|
+
});
|
|
584
|
+
assert.deepEqual(identity, {
|
|
585
|
+
version: '8.0.0.632',
|
|
586
|
+
major: '8',
|
|
587
|
+
source: 'executable-metadata'
|
|
588
|
+
});
|
|
589
|
+
} finally {
|
|
590
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
574
594
|
test('classic GeneXus installation discovery accepts GENEXUS_HOME with gx.exe', () => {
|
|
575
595
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-classic-home-'));
|
|
576
596
|
const previousHome = process.env.GENEXUS_HOME;
|
|
@@ -692,6 +712,72 @@ test('KB identity reads GeneXus 9.0 classic major from gxi when gxw is absent',
|
|
|
692
712
|
}
|
|
693
713
|
});
|
|
694
714
|
|
|
715
|
+
test('classic DAT KB identity recognizes GX8/9 model markers without inventing a major', () => {
|
|
716
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-kb-classic-dat-'));
|
|
717
|
+
try {
|
|
718
|
+
for (const marker of ['DATA001', 'GXSPC001', 'ATTRIBUT.DAT', 'ATT.XPW']) {
|
|
719
|
+
fs.writeFileSync(path.join(tempRoot, marker), 'classic');
|
|
720
|
+
}
|
|
721
|
+
const identity = readGeneXusKbIdentity(tempRoot);
|
|
722
|
+
assert.equal(identity.major, null);
|
|
723
|
+
assert.equal(identity.source, 'classic-dat');
|
|
724
|
+
assert.equal(identity.reason, 'classic-generation-requires-provider');
|
|
725
|
+
assert.equal(directoryLooksLikeKnowledgeBase(tempRoot), true);
|
|
726
|
+
} finally {
|
|
727
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
test('KB catalog edits preserve per-KB legacy driver metadata', () => {
|
|
732
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-kb-catalog-'));
|
|
733
|
+
const configPath = path.join(tempRoot, 'config.json');
|
|
734
|
+
try {
|
|
735
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
736
|
+
Environment: {
|
|
737
|
+
KBs: {
|
|
738
|
+
sect80: {
|
|
739
|
+
Path: 'D:\\\\GX80\\\\SECT',
|
|
740
|
+
Driver: 'com-gxpublic',
|
|
741
|
+
InstallationPath: 'C:\\\\gxw80',
|
|
742
|
+
Major: '8'
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
ActiveKb: 'sect80'
|
|
746
|
+
}
|
|
747
|
+
}));
|
|
748
|
+
|
|
749
|
+
const catalog = readKbCatalog(configPath);
|
|
750
|
+
assert.equal(catalog.kbs.sect80, 'D:\\\\GX80\\\\SECT');
|
|
751
|
+
assert.equal(Object.keys(catalog.kbs).length, 1);
|
|
752
|
+
const switched = switchActiveKb(configPath, { name: 'sect80' });
|
|
753
|
+
assert.equal(switched.ok, true);
|
|
754
|
+
const persisted = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
755
|
+
assert.equal(persisted.Environment.KBs.sect80.Driver, 'com-gxpublic');
|
|
756
|
+
assert.equal(persisted.Environment.KBs.sect80.InstallationPath, 'C:\\\\gxw80');
|
|
757
|
+
assert.equal(persisted.Environment.KBs.sect80.Major, '8');
|
|
758
|
+
} finally {
|
|
759
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
test('classic gxi identity does not invent a GX8/GX9 mismatch before provider open', () => {
|
|
764
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-kb-gxi-compare-'));
|
|
765
|
+
const gx8Root = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-gx8-compare-'));
|
|
766
|
+
try {
|
|
767
|
+
fs.writeFileSync(path.join(tempRoot, 'Legacy.gxi'), 'classic-kb');
|
|
768
|
+
fs.writeFileSync(path.join(gx8Root, 'gxw32.exe'), 'not-a-real-executable');
|
|
769
|
+
const compatibility = compareGeneXusKbAndInstallation(tempRoot, gx8Root, {
|
|
770
|
+
readExecutableVersion: (exePath) => exePath.endsWith('gxw32.exe') ? '8.0.0.632' : null
|
|
771
|
+
});
|
|
772
|
+
assert.equal(compatibility.status, 'unresolved');
|
|
773
|
+
assert.equal(compatibility.kb.source, 'gxi-classic');
|
|
774
|
+
assert.equal(compatibility.gx.major, '8');
|
|
775
|
+
} finally {
|
|
776
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
777
|
+
removeTempPath(gx8Root, { recursive: true, force: true });
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
|
|
695
781
|
test('init rejects a known KB and SDK major mismatch before writing config', () => {
|
|
696
782
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-mismatch-'));
|
|
697
783
|
const kbDir = path.join(tempRoot, 'kb17');
|
|
@@ -1088,6 +1174,146 @@ test('--fields validation returns usage error for invalid doctor field', () => {
|
|
|
1088
1174
|
assert.equal(parsed.error.code, 'usage_error');
|
|
1089
1175
|
});
|
|
1090
1176
|
|
|
1177
|
+
test('doctor client_config_sync describes the effective gateway and its source', () => {
|
|
1178
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-doctor-gateway-source-'));
|
|
1179
|
+
try {
|
|
1180
|
+
const env = sandboxHomeEnv(tempRoot);
|
|
1181
|
+
const configuredGateway = path.join(tempRoot, 'checkout', 'publish', 'GxMcp.Gateway.exe');
|
|
1182
|
+
const mismatchedGateway = path.join(tempRoot, 'other', 'GxMcp.Gateway.exe');
|
|
1183
|
+
const clientConfig = path.join(tempRoot, '.gemini', 'settings.json');
|
|
1184
|
+
fs.mkdirSync(path.dirname(configuredGateway), { recursive: true });
|
|
1185
|
+
fs.mkdirSync(path.dirname(clientConfig), { recursive: true });
|
|
1186
|
+
fs.writeFileSync(configuredGateway, '');
|
|
1187
|
+
fs.writeFileSync(clientConfig, JSON.stringify({
|
|
1188
|
+
mcpServers: {
|
|
1189
|
+
genexus18mcp: { command: configuredGateway, args: [] }
|
|
1190
|
+
}
|
|
1191
|
+
}));
|
|
1192
|
+
|
|
1193
|
+
const overrideEnv = { ...env, GENEXUS_MCP_GATEWAY_EXE: configuredGateway };
|
|
1194
|
+
const matching = runCli(['doctor', '--format', 'json'], { env: overrideEnv });
|
|
1195
|
+
assert.equal(matching.status, 0);
|
|
1196
|
+
const matchingCheck = JSON.parse(matching.stdout).ok.checks.find((c) => c.id === 'client_config_sync');
|
|
1197
|
+
assert.ok(matchingCheck);
|
|
1198
|
+
assert.equal(matchingCheck.status, 'pass');
|
|
1199
|
+
assert.ok(matchingCheck.detail.includes(`Configured Gateway: ${configuredGateway} (source: GENEXUS_MCP_GATEWAY_EXE)`));
|
|
1200
|
+
assert.doesNotMatch(matchingCheck.detail, /npm-package gateway exe/);
|
|
1201
|
+
|
|
1202
|
+
fs.writeFileSync(clientConfig, JSON.stringify({
|
|
1203
|
+
mcpServers: {
|
|
1204
|
+
genexus18mcp: { command: mismatchedGateway, args: [] }
|
|
1205
|
+
}
|
|
1206
|
+
}));
|
|
1207
|
+
const mismatching = runCli(['doctor', '--format', 'json'], { env: overrideEnv });
|
|
1208
|
+
assert.equal(mismatching.status, 0);
|
|
1209
|
+
const mismatchingCheck = JSON.parse(mismatching.stdout).ok.checks.find((c) => c.id === 'client_config_sync');
|
|
1210
|
+
assert.ok(mismatchingCheck);
|
|
1211
|
+
assert.equal(mismatchingCheck.status, 'warn');
|
|
1212
|
+
assert.ok(mismatchingCheck.detail.includes(`Configured Gateway: ${configuredGateway} (source: GENEXUS_MCP_GATEWAY_EXE)`));
|
|
1213
|
+
assert.doesNotMatch(mismatchingCheck.detail, /this npm package's bundled exe/);
|
|
1214
|
+
} finally {
|
|
1215
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
test('doctor fails closed when GX_CONFIG_PATH points at a missing file', () => {
|
|
1220
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-doctor-config-selection-'));
|
|
1221
|
+
try {
|
|
1222
|
+
const cwdConfig = path.join(tempRoot, 'config.json');
|
|
1223
|
+
const missingConfig = path.join(tempRoot, 'missing.json');
|
|
1224
|
+
fs.writeFileSync(cwdConfig, JSON.stringify({ Environment: { KBPath: 'C:\\wrong-context' } }));
|
|
1225
|
+
|
|
1226
|
+
const result = runCli(['doctor', '--mcp-smoke', '--format', 'json'], {
|
|
1227
|
+
cwd: tempRoot,
|
|
1228
|
+
env: {
|
|
1229
|
+
...sandboxHomeEnv(tempRoot),
|
|
1230
|
+
GX_CONFIG_PATH: missingConfig,
|
|
1231
|
+
GENEXUS_MCP_GATEWAY_EXE: process.execPath
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
assert.equal(result.status, 0);
|
|
1235
|
+
const checks = JSON.parse(result.stdout).ok.checks;
|
|
1236
|
+
const configCheck = checks.find((c) => c.id === 'config_file');
|
|
1237
|
+
const probeCheck = checks.find((c) => c.id === 'gateway_spawn_probe');
|
|
1238
|
+
const smokeCheck = checks.find((c) => c.id === 'mcp_smoke');
|
|
1239
|
+
assert.ok(configCheck);
|
|
1240
|
+
assert.equal(configCheck.status, 'fail');
|
|
1241
|
+
assert.ok(configCheck.detail.includes(missingConfig));
|
|
1242
|
+
assert.ok(probeCheck);
|
|
1243
|
+
assert.equal(probeCheck.status, 'not_applicable');
|
|
1244
|
+
assert.ok(smokeCheck);
|
|
1245
|
+
assert.equal(smokeCheck.status, 'not_applicable');
|
|
1246
|
+
} finally {
|
|
1247
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
test('doctor rejects malformed tool definitions instead of reporting only file presence', () => {
|
|
1252
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-doctor-tool-defs-'));
|
|
1253
|
+
try {
|
|
1254
|
+
const configPath = path.join(tempRoot, 'config.json');
|
|
1255
|
+
const toolDefinitionsPath = path.join(tempRoot, 'tool_definitions.json');
|
|
1256
|
+
fs.writeFileSync(configPath, JSON.stringify({}));
|
|
1257
|
+
fs.writeFileSync(toolDefinitionsPath, '{ invalid json');
|
|
1258
|
+
|
|
1259
|
+
const result = runCli(['doctor', '--format', 'json'], {
|
|
1260
|
+
cwd: tempRoot,
|
|
1261
|
+
env: {
|
|
1262
|
+
...sandboxHomeEnv(tempRoot),
|
|
1263
|
+
GX_CONFIG_PATH: configPath,
|
|
1264
|
+
GENEXUS_MCP_GATEWAY_EXE: path.join(tempRoot, 'missing', 'GxMcp.Gateway.exe'),
|
|
1265
|
+
GENEXUS_MCP_TOOL_DEFINITIONS: toolDefinitionsPath
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
assert.equal(result.status, 0);
|
|
1269
|
+
const check = JSON.parse(result.stdout).ok.checks.find((c) => c.id === 'tool_definitions');
|
|
1270
|
+
assert.ok(check);
|
|
1271
|
+
assert.equal(check.status, 'fail');
|
|
1272
|
+
assert.ok(check.detail.includes(toolDefinitionsPath));
|
|
1273
|
+
} finally {
|
|
1274
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
test('doctor reports strict KB catalog ambiguity instead of ignoring Environment.KBs', () => {
|
|
1279
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-doctor-kb-catalog-'));
|
|
1280
|
+
try {
|
|
1281
|
+
const kbOne = path.join(tempRoot, 'kb-one');
|
|
1282
|
+
const kbTwo = path.join(tempRoot, 'kb-two');
|
|
1283
|
+
const configPath = path.join(tempRoot, 'config.json');
|
|
1284
|
+
fs.mkdirSync(kbOne, { recursive: true });
|
|
1285
|
+
fs.mkdirSync(kbTwo, { recursive: true });
|
|
1286
|
+
fs.writeFileSync(path.join(kbOne, 'model.gxw'), '');
|
|
1287
|
+
fs.writeFileSync(path.join(kbTwo, 'model.gxw'), '');
|
|
1288
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
1289
|
+
GatewayMode: 'stdio-isolated',
|
|
1290
|
+
Environment: {
|
|
1291
|
+
ResolutionPolicy: 'strict',
|
|
1292
|
+
KBs: {
|
|
1293
|
+
one: { Path: kbOne },
|
|
1294
|
+
two: { Path: kbTwo }
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}));
|
|
1298
|
+
|
|
1299
|
+
const result = runCli(['doctor', '--format', 'json'], {
|
|
1300
|
+
cwd: tempRoot,
|
|
1301
|
+
env: {
|
|
1302
|
+
...sandboxHomeEnv(tempRoot),
|
|
1303
|
+
GX_CONFIG_PATH: configPath,
|
|
1304
|
+
GENEXUS_MCP_GATEWAY_EXE: path.join(tempRoot, 'missing', 'GxMcp.Gateway.exe')
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
assert.equal(result.status, 0);
|
|
1308
|
+
const check = JSON.parse(result.stdout).ok.checks.find((c) => c.id === 'kb_catalog');
|
|
1309
|
+
assert.ok(check);
|
|
1310
|
+
assert.equal(check.status, 'warn');
|
|
1311
|
+
assert.match(check.detail, /neither ActiveKb nor DefaultKb/);
|
|
1312
|
+
} finally {
|
|
1313
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1091
1317
|
test('doctor finds tool_definitions.json next to the gateway exe (not just dev-tree)', () => {
|
|
1092
1318
|
// Regression for v2.6.6 bug: getToolDefinitionsPath() hard-coded the dev-tree
|
|
1093
1319
|
// location, so every installed copy reported "tool_definitions.json is missing"
|
package/config/gx-versions.json
CHANGED
|
@@ -63,15 +63,17 @@
|
|
|
63
63
|
"driver": "com-gxpublic",
|
|
64
64
|
"defaultInstallPath": "C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 9.0",
|
|
65
65
|
"registryNames": ["GeneXus 9.0"],
|
|
66
|
-
"legacyRegistryVersions": ["9.0"]
|
|
66
|
+
"legacyRegistryVersions": ["9.0"],
|
|
67
|
+
"legacySetupVersions": ["90"]
|
|
67
68
|
},
|
|
68
69
|
{
|
|
69
70
|
"major": "8",
|
|
70
71
|
"displayName": "GeneXus 8.0",
|
|
71
72
|
"driver": "com-gxpublic",
|
|
72
|
-
"defaultInstallPath": "C:\\Program Files (x86)\\ARTech\\GeneXus\\
|
|
73
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\ARTech\\GeneXus\\gxw80",
|
|
73
74
|
"registryNames": ["GeneXus 8.0"],
|
|
74
|
-
"legacyRegistryVersions": ["8.0"]
|
|
75
|
+
"legacyRegistryVersions": ["8.0"],
|
|
76
|
+
"legacySetupVersions": ["80"]
|
|
75
77
|
}
|
|
76
78
|
]
|
|
77
79
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genexus-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"mcpName": "io.github.lennix1337/genexus",
|
|
5
5
|
"description": "GeneXus 16, GeneXus 17, GeneXus 18 MCP server with native SDK support; basic legacy compatibility for GeneXus Evolution 3, GeneXus Evolution 2, GeneXus Evolution 1, GeneXus 15, GeneXus 9.0, GeneXus 8.0 via com-gxpublic and dotnet-reflection drivers — read, edit, and analyze Knowledge Base objects directly from Claude, Cursor, and other AI agents over the Model Context Protocol.",
|
|
6
6
|
"keywords": [
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"targets": {
|
|
8
8
|
".NETCoreApp,Version=v10.0": {},
|
|
9
9
|
".NETCoreApp,Version=v10.0/win-x64": {
|
|
10
|
-
"GxMcp.Gateway/3.
|
|
10
|
+
"GxMcp.Gateway/3.7.0": {
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"Newtonsoft.Json": "13.0.3",
|
|
13
13
|
"System.Management": "10.0.5",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
"libraries": {
|
|
69
|
-
"GxMcp.Gateway/3.
|
|
69
|
+
"GxMcp.Gateway/3.7.0": {
|
|
70
70
|
"type": "project",
|
|
71
71
|
"serviceable": false,
|
|
72
72
|
"sha512": ""
|
|
Binary file
|
|
Binary file
|
|
@@ -63,15 +63,17 @@
|
|
|
63
63
|
"driver": "com-gxpublic",
|
|
64
64
|
"defaultInstallPath": "C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 9.0",
|
|
65
65
|
"registryNames": ["GeneXus 9.0"],
|
|
66
|
-
"legacyRegistryVersions": ["9.0"]
|
|
66
|
+
"legacyRegistryVersions": ["9.0"],
|
|
67
|
+
"legacySetupVersions": ["90"]
|
|
67
68
|
},
|
|
68
69
|
{
|
|
69
70
|
"major": "8",
|
|
70
71
|
"displayName": "GeneXus 8.0",
|
|
71
72
|
"driver": "com-gxpublic",
|
|
72
|
-
"defaultInstallPath": "C:\\Program Files (x86)\\ARTech\\GeneXus\\
|
|
73
|
+
"defaultInstallPath": "C:\\Program Files (x86)\\ARTech\\GeneXus\\gxw80",
|
|
73
74
|
"registryNames": ["GeneXus 8.0"],
|
|
74
|
-
"legacyRegistryVersions": ["8.0"]
|
|
75
|
+
"legacyRegistryVersions": ["8.0"],
|
|
76
|
+
"legacySetupVersions": ["80"]
|
|
75
77
|
}
|
|
76
78
|
]
|
|
77
79
|
}
|
package/publish/config.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
2
|
+
"GatewayMode": "stdio-isolated",
|
|
3
|
+
"ConfigSchemaVersion": 2,
|
|
4
|
+
"GeneXus": {
|
|
5
|
+
"WorkerExecutable": "worker\\\\GxMcp.Worker.exe",
|
|
6
|
+
"InstallationPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus18"
|
|
4
7
|
},
|
|
5
8
|
"Server": {
|
|
6
|
-
"McpStdio": true,
|
|
7
|
-
"TerseResponses": true,
|
|
8
|
-
"HttpPort": 0,
|
|
9
9
|
"WorkerIdleTimeoutMinutes": 5,
|
|
10
|
+
"HttpPort": 0,
|
|
11
|
+
"TerseResponses": true,
|
|
10
12
|
"SessionIdleTimeoutMinutes": 10,
|
|
11
|
-
"EmitStructuredContent": false
|
|
12
|
-
|
|
13
|
-
"ConfigSchemaVersion": 2,
|
|
14
|
-
"GeneXus": {
|
|
15
|
-
"InstallationPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus18",
|
|
16
|
-
"WorkerExecutable": "worker\\\\GxMcp.Worker.exe"
|
|
13
|
+
"EmitStructuredContent": false,
|
|
14
|
+
"McpStdio": true
|
|
17
15
|
},
|
|
18
|
-
"
|
|
16
|
+
"Environment": {
|
|
17
|
+
"ResolutionPolicy": "strict"
|
|
18
|
+
}
|
|
19
19
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "gxmcp-release-manifest/1",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"sourceCommit": "
|
|
3
|
+
"version": "3.7.0",
|
|
4
|
+
"sourceCommit": "1c6d0fb48c98ce2e88b5914a8b6f11510a0c9691",
|
|
5
5
|
"sourceCommitPolicy": "exact-tag",
|
|
6
|
-
"generatedAtUtc": "2026-09-
|
|
6
|
+
"generatedAtUtc": "2026-09-19T05:58:25.2316950Z",
|
|
7
7
|
"runtime": {
|
|
8
8
|
"gateway": "net10.0-windows",
|
|
9
9
|
"worker": "net48-x86",
|
|
@@ -15,28 +15,28 @@
|
|
|
15
15
|
"2026-07-28"
|
|
16
16
|
],
|
|
17
17
|
"schema": "tool_definitions.json",
|
|
18
|
-
"schemaSha256": "
|
|
18
|
+
"schemaSha256": "de5ac0b8955a64065f4d9468b6b4e66610f476e43c9732c1c233b49feee5ddbb",
|
|
19
19
|
"provenance": "gxmcp-sbom.json",
|
|
20
20
|
"artifacts": [
|
|
21
21
|
{
|
|
22
22
|
"path": "gxmcp-sbom.json",
|
|
23
23
|
"size": 590,
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "7d32db9bbf7bab298561c8e5fe9f2e2518e4ac716daa371aa4dbd752ce61b300"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"path": "GxMcp.Gateway.exe",
|
|
28
28
|
"size": 162304,
|
|
29
|
-
"sha256": "
|
|
29
|
+
"sha256": "3507a7154b251a53deaadbb524298b1958ef017f4d59fc3ac369f04652ec9ac2"
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
"path": "tool_definitions.json",
|
|
33
|
-
"size":
|
|
34
|
-
"sha256": "
|
|
33
|
+
"size": 126023,
|
|
34
|
+
"sha256": "de5ac0b8955a64065f4d9468b6b4e66610f476e43c9732c1c233b49feee5ddbb"
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"path": "worker/GxMcp.Worker.exe",
|
|
38
|
-
"size":
|
|
39
|
-
"sha256": "
|
|
38
|
+
"size": 3414016,
|
|
39
|
+
"sha256": "9c1bb1f040661936e367706f353e5da14d0cc2e3ac0217b3b0ae1b5919fc5ce6"
|
|
40
40
|
}
|
|
41
41
|
]
|
|
42
42
|
}
|