genexus-mcp 3.6.0 → 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 +136 -24
- package/cli/commands/axi.js +311 -64
- package/cli/lib/config.js +129 -32
- package/cli/run.test.js +258 -0
- package/config/gx-versions.json +5 -3
- package/package.json +2 -2
- 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 +6 -6
- package/publish/gxmcp-manifest.json +10 -10
- package/publish/gxmcp-sbom.json +4 -4
- package/publish/tool_definitions.json +5 -5
- 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,12 +112,17 @@ 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
|
|
|
121
|
+
function hasGeneXusExecutable(gxPath) {
|
|
122
|
+
return Boolean(gxPath) && ['genexus.exe', 'gx.exe', 'gxw32.exe']
|
|
123
|
+
.some((name) => fs.existsSync(path.join(gxPath, name)));
|
|
124
|
+
}
|
|
125
|
+
|
|
116
126
|
function getGeneXusCatalogEntries(preferredMajor = null) {
|
|
117
127
|
const catalog = getGeneXusVersionCatalog();
|
|
118
128
|
const all = [...catalog.supportedMajors, ...(catalog.legacyMajors || [])];
|
|
@@ -153,13 +163,31 @@ function discoverGeneXusFromRegistry(preferredMajor = null) {
|
|
|
153
163
|
const match = out.match(/InstallationDirectory\s+REG_SZ\s+(.+?)\r?\n/i);
|
|
154
164
|
if (match) {
|
|
155
165
|
const candidate = match[1].trim().replace(/[\\/]+$/, '');
|
|
156
|
-
if (candidate &&
|
|
166
|
+
if (candidate && hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
|
|
157
167
|
return candidate;
|
|
158
168
|
}
|
|
159
169
|
}
|
|
160
170
|
} catch {
|
|
161
171
|
}
|
|
162
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
|
+
}
|
|
163
191
|
for (const legacyVersion of (entry.legacyRegistryVersions || [])) {
|
|
164
192
|
const key = `${hive}\\GeneXus\\${legacyVersion}`;
|
|
165
193
|
try {
|
|
@@ -172,7 +200,7 @@ function discoverGeneXusFromRegistry(preferredMajor = null) {
|
|
|
172
200
|
const match = out.match(/InstallPath\s+REG_SZ\s+(.+?)\r?\n/i);
|
|
173
201
|
if (match) {
|
|
174
202
|
const candidate = match[1].trim().replace(/[\\/]+$/, '');
|
|
175
|
-
if (candidate &&
|
|
203
|
+
if (candidate && hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) return candidate;
|
|
176
204
|
}
|
|
177
205
|
} catch {
|
|
178
206
|
}
|
|
@@ -187,7 +215,7 @@ function discoverGeneXusFromRegistry(preferredMajor = null) {
|
|
|
187
215
|
function discoverGeneXusInstallation(preferredMajor = null) {
|
|
188
216
|
if (process.env.GENEXUS_HOME) {
|
|
189
217
|
const candidate = process.env.GENEXUS_HOME.replace(/[\\/]+$/, '');
|
|
190
|
-
if (
|
|
218
|
+
if (hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) return candidate;
|
|
191
219
|
}
|
|
192
220
|
|
|
193
221
|
const fromRegistry = discoverGeneXusFromRegistry(preferredMajor);
|
|
@@ -205,7 +233,11 @@ function discoverGeneXusInstallation(preferredMajor = null) {
|
|
|
205
233
|
const entries = getGeneXusCatalogEntries(preferredMajor);
|
|
206
234
|
const seen = new Set();
|
|
207
235
|
for (const base of programDirs) {
|
|
208
|
-
const
|
|
236
|
+
const roots = [
|
|
237
|
+
path.join(base, 'GeneXus'),
|
|
238
|
+
path.join(base, 'Artech', 'GeneXus')
|
|
239
|
+
];
|
|
240
|
+
for (const root of roots) {
|
|
209
241
|
const key = root.toLowerCase();
|
|
210
242
|
if (seen.has(key)) continue;
|
|
211
243
|
seen.add(key);
|
|
@@ -217,7 +249,7 @@ function discoverGeneXusInstallation(preferredMajor = null) {
|
|
|
217
249
|
for (const ver of candidateNames) {
|
|
218
250
|
if (!ver) continue;
|
|
219
251
|
const candidate = path.join(root, ver);
|
|
220
|
-
if (
|
|
252
|
+
if (hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
|
|
221
253
|
return candidate;
|
|
222
254
|
}
|
|
223
255
|
}
|
|
@@ -230,13 +262,14 @@ function discoverGeneXusInstallation(preferredMajor = null) {
|
|
|
230
262
|
const majorMatch = entry.match(/^GeneXus\s*(\d+)/i);
|
|
231
263
|
if (majorMatch && !entries.some((item) => String(item.major) === majorMatch[1])) continue;
|
|
232
264
|
const candidate = path.join(root, entry);
|
|
233
|
-
if (
|
|
265
|
+
if (hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
|
|
234
266
|
return candidate;
|
|
235
267
|
}
|
|
236
268
|
}
|
|
237
269
|
}
|
|
238
270
|
} catch {
|
|
239
271
|
}
|
|
272
|
+
}
|
|
240
273
|
}
|
|
241
274
|
|
|
242
275
|
const fromPath = discoverGeneXusFromPath(preferredMajor);
|
|
@@ -249,15 +282,22 @@ function discoverGeneXusFromPath(preferredMajor = null) {
|
|
|
249
282
|
if (process.platform !== 'win32') return null;
|
|
250
283
|
try {
|
|
251
284
|
const { execFileSync } = require('child_process');
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
285
|
+
for (const executable of ['genexus.exe', 'gx.exe', 'gxw32.exe']) {
|
|
286
|
+
let out;
|
|
287
|
+
try {
|
|
288
|
+
out = execFileSync('where.exe', [executable], {
|
|
289
|
+
encoding: 'utf8',
|
|
290
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
291
|
+
windowsHide: true,
|
|
292
|
+
timeout: 3000
|
|
293
|
+
});
|
|
294
|
+
} catch {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const first = out.split('\n').map((s) => s.trim()).find(Boolean);
|
|
298
|
+
if (first && fs.existsSync(first) && matchesPreferredGeneXusMajor(path.dirname(first), preferredMajor)) {
|
|
299
|
+
return path.dirname(first);
|
|
300
|
+
}
|
|
261
301
|
}
|
|
262
302
|
} catch {
|
|
263
303
|
}
|
|
@@ -344,7 +384,9 @@ function discoverKnowledgeBases(cwd, { maxResults = 25, scanDepth = 2 } = {}) {
|
|
|
344
384
|
function directoryLooksLikeKnowledgeBase(dir) {
|
|
345
385
|
try {
|
|
346
386
|
const files = fs.readdirSync(dir);
|
|
347
|
-
|
|
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;
|
|
348
390
|
} catch {
|
|
349
391
|
return false;
|
|
350
392
|
}
|
|
@@ -586,8 +628,11 @@ function migrateLegacyConfig(sourcePath, targetPath, { rejectNonMigratable = fal
|
|
|
586
628
|
|
|
587
629
|
function resolveConfigPathNoMutate(cwd) {
|
|
588
630
|
const cwdConfigPath = path.join(cwd, 'config.json');
|
|
589
|
-
|
|
590
|
-
|
|
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);
|
|
591
636
|
}
|
|
592
637
|
if (fs.existsSync(cwdConfigPath)) {
|
|
593
638
|
return cwdConfigPath;
|
|
@@ -1910,6 +1955,9 @@ function readGeneXusInstallationIdentity(gxPath, options = {}) {
|
|
|
1910
1955
|
if (!executableVersion) {
|
|
1911
1956
|
executableVersion = readExecutableVersion(path.join(gxPath, 'gx.exe'));
|
|
1912
1957
|
}
|
|
1958
|
+
if (!executableVersion) {
|
|
1959
|
+
executableVersion = readExecutableVersion(path.join(gxPath, 'gxw32.exe'));
|
|
1960
|
+
}
|
|
1913
1961
|
if (executableVersion) {
|
|
1914
1962
|
return {
|
|
1915
1963
|
version: executableVersion,
|
|
@@ -1996,6 +2044,7 @@ function readGeneXusKbIdentity(kbPath) {
|
|
|
1996
2044
|
|
|
1997
2045
|
let gxwFiles;
|
|
1998
2046
|
let gxiFiles = [];
|
|
2047
|
+
let classicMarkerCount = 0;
|
|
1999
2048
|
try {
|
|
2000
2049
|
const allFiles = fs.readdirSync(kbPath);
|
|
2001
2050
|
gxwFiles = allFiles
|
|
@@ -2004,6 +2053,8 @@ function readGeneXusKbIdentity(kbPath) {
|
|
|
2004
2053
|
gxiFiles = allFiles
|
|
2005
2054
|
.filter((fileName) => fileName.toLowerCase().endsWith('.gxi'))
|
|
2006
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;
|
|
2007
2058
|
} catch {
|
|
2008
2059
|
return { version: null, major: null, source: 'unavailable', reason: 'unreadable-kb-path' };
|
|
2009
2060
|
}
|
|
@@ -2011,6 +2062,9 @@ function readGeneXusKbIdentity(kbPath) {
|
|
|
2011
2062
|
if (gxiFiles.length > 0) {
|
|
2012
2063
|
return { version: '9.0', major: '9', source: 'gxi-classic', reason: null };
|
|
2013
2064
|
}
|
|
2065
|
+
if (classicMarkerCount >= 2) {
|
|
2066
|
+
return { version: null, major: null, source: 'classic-dat', reason: 'classic-generation-requires-provider' };
|
|
2067
|
+
}
|
|
2014
2068
|
return { version: null, major: null, source: 'unavailable', reason: 'no-gxw' };
|
|
2015
2069
|
}
|
|
2016
2070
|
if (gxwFiles.length > 1) return { version: null, major: null, source: 'unavailable', reason: 'multiple-gxw' };
|
|
@@ -2032,34 +2086,65 @@ function readGeneXusKbIdentity(kbPath) {
|
|
|
2032
2086
|
}
|
|
2033
2087
|
}
|
|
2034
2088
|
|
|
2035
|
-
function compareGeneXusKbAndInstallation(kbPath, gxPath) {
|
|
2089
|
+
function compareGeneXusKbAndInstallation(kbPath, gxPath, options = {}) {
|
|
2036
2090
|
const kb = readGeneXusKbIdentity(kbPath);
|
|
2037
|
-
const gx = readGeneXusInstallationIdentity(gxPath);
|
|
2091
|
+
const gx = readGeneXusInstallationIdentity(gxPath, options);
|
|
2038
2092
|
let status = 'unresolved';
|
|
2039
|
-
|
|
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
|
+
}
|
|
2040
2101
|
return { status, kb, gx };
|
|
2041
2102
|
}
|
|
2042
2103
|
|
|
2043
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
|
+
|
|
2044
2128
|
if (Array.isArray(raw)) {
|
|
2045
|
-
const normalized = {};
|
|
2046
2129
|
for (const entry of raw) {
|
|
2047
2130
|
if (!entry || typeof entry !== 'object') continue;
|
|
2048
2131
|
const name = typeof entry.alias === 'string' ? entry.alias : entry.Alias;
|
|
2049
|
-
|
|
2050
|
-
if (typeof name === 'string' && name && typeof kbPath === 'string' && kbPath) {
|
|
2051
|
-
normalized[name] = kbPath;
|
|
2052
|
-
}
|
|
2132
|
+
add(name, entry);
|
|
2053
2133
|
}
|
|
2134
|
+
Object.defineProperty(normalized, '__entries', { value: entries, enumerable: false, writable: true });
|
|
2054
2135
|
return normalized;
|
|
2055
2136
|
}
|
|
2056
2137
|
|
|
2057
|
-
|
|
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;
|
|
2058
2143
|
}
|
|
2059
2144
|
|
|
2060
|
-
function readKbCatalog(configPath) {
|
|
2061
|
-
if (!configPath) return { kbs: {}, activeKb: null, kbPath: null };
|
|
2062
|
-
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;
|
|
2063
2148
|
if (!cfg) return { kbs: {}, activeKb: null, kbPath: null };
|
|
2064
2149
|
const env = cfg.Environment || {};
|
|
2065
2150
|
const activeKb = typeof env.ActiveKb === 'string' && env.ActiveKb
|
|
@@ -2075,7 +2160,19 @@ function readKbCatalog(configPath) {
|
|
|
2075
2160
|
function writeKbCatalog(configPath, { kbs, activeKb, kbPath }) {
|
|
2076
2161
|
const cfg = readJsonFileSafe(configPath) || {};
|
|
2077
2162
|
cfg.Environment = cfg.Environment || {};
|
|
2078
|
-
|
|
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;
|
|
2079
2176
|
if (activeKb) {
|
|
2080
2177
|
cfg.Environment.ActiveKb = activeKb;
|
|
2081
2178
|
cfg.Environment.DefaultKb = activeKb;
|
package/cli/run.test.js
CHANGED
|
@@ -20,9 +20,13 @@ const {
|
|
|
20
20
|
getLauncher,
|
|
21
21
|
getGeneXusMajor,
|
|
22
22
|
getGeneXusCatalogEntries,
|
|
23
|
+
discoverGeneXusInstallation,
|
|
23
24
|
readGeneXusInstallationIdentity,
|
|
25
|
+
directoryLooksLikeKnowledgeBase,
|
|
24
26
|
readGeneXusKbIdentity,
|
|
25
27
|
compareGeneXusKbAndInstallation,
|
|
28
|
+
readKbCatalog,
|
|
29
|
+
switchActiveKb,
|
|
26
30
|
patchClientConfig
|
|
27
31
|
} = require('./lib/config');
|
|
28
32
|
const { handleInit, resolveMcpSmokeTarget } = require('./commands/axi');
|
|
@@ -553,6 +557,54 @@ test('GeneXus installation identity falls back to executable metadata', () => {
|
|
|
553
557
|
}
|
|
554
558
|
});
|
|
555
559
|
|
|
560
|
+
test('classic GeneXus installation identity reads gx.exe metadata', () => {
|
|
561
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-classic-version-'));
|
|
562
|
+
try {
|
|
563
|
+
fs.writeFileSync(path.join(tempRoot, 'gx.exe'), 'not-a-real-executable');
|
|
564
|
+
const identity = readGeneXusInstallationIdentity(tempRoot, {
|
|
565
|
+
readExecutableVersion: (exePath) => exePath.endsWith('gx.exe') ? '9.0.123' : null
|
|
566
|
+
});
|
|
567
|
+
assert.deepEqual(identity, {
|
|
568
|
+
version: '9.0.123',
|
|
569
|
+
major: '9',
|
|
570
|
+
source: 'executable-metadata'
|
|
571
|
+
});
|
|
572
|
+
} finally {
|
|
573
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
574
|
+
}
|
|
575
|
+
});
|
|
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
|
+
|
|
594
|
+
test('classic GeneXus installation discovery accepts GENEXUS_HOME with gx.exe', () => {
|
|
595
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-classic-home-'));
|
|
596
|
+
const previousHome = process.env.GENEXUS_HOME;
|
|
597
|
+
try {
|
|
598
|
+
fs.writeFileSync(path.join(tempRoot, 'gx.exe'), 'not-a-real-executable');
|
|
599
|
+
process.env.GENEXUS_HOME = tempRoot;
|
|
600
|
+
assert.equal(discoverGeneXusInstallation(), tempRoot);
|
|
601
|
+
} finally {
|
|
602
|
+
if (previousHome === undefined) delete process.env.GENEXUS_HOME;
|
|
603
|
+
else process.env.GENEXUS_HOME = previousHome;
|
|
604
|
+
removeTempPath(tempRoot, { recursive: true, force: true });
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
556
608
|
test('GeneXus installation identity ignores an invalid version file when executable metadata is valid', () => {
|
|
557
609
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-invalid-version-'));
|
|
558
610
|
try {
|
|
@@ -660,6 +712,72 @@ test('KB identity reads GeneXus 9.0 classic major from gxi when gxw is absent',
|
|
|
660
712
|
}
|
|
661
713
|
});
|
|
662
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
|
+
|
|
663
781
|
test('init rejects a known KB and SDK major mismatch before writing config', () => {
|
|
664
782
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-mismatch-'));
|
|
665
783
|
const kbDir = path.join(tempRoot, 'kb17');
|
|
@@ -1056,6 +1174,146 @@ test('--fields validation returns usage error for invalid doctor field', () => {
|
|
|
1056
1174
|
assert.equal(parsed.error.code, 'usage_error');
|
|
1057
1175
|
});
|
|
1058
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
|
+
|
|
1059
1317
|
test('doctor finds tool_definitions.json next to the gateway exe (not just dev-tree)', () => {
|
|
1060
1318
|
// Regression for v2.6.6 bug: getToolDefinitionsPath() hard-coded the dev-tree
|
|
1061
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,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genexus-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"mcpName": "io.github.lennix1337/genexus",
|
|
5
|
-
"description": "GeneXus 16, GeneXus 17, GeneXus 18 MCP server — read, edit, and analyze Knowledge Base objects directly from Claude, Cursor, and other AI agents over the Model Context Protocol.",
|
|
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
|
"mcp",
|
|
8
8
|
"mcp-server",
|
|
@@ -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
|