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/commands/axi.js
CHANGED
|
@@ -152,30 +152,51 @@ function buildStatusData(cwd) {
|
|
|
152
152
|
const configPath = resolveConfigPathNoMutate(cwd);
|
|
153
153
|
const gatewayExePath = getGatewayExePath();
|
|
154
154
|
const gatewayExeFound = fs.existsSync(gatewayExePath);
|
|
155
|
-
const configFound = !!configPath;
|
|
155
|
+
const configFound = !!(configPath && fs.existsSync(configPath));
|
|
156
156
|
|
|
157
157
|
let kbLooksValid = false;
|
|
158
158
|
let kbPath = null;
|
|
159
|
+
let kbCatalog = { kbs: {}, activeKb: null, kbPath: null };
|
|
159
160
|
let gxPath = null;
|
|
160
161
|
let configSource = null;
|
|
162
|
+
let configReadable = false;
|
|
163
|
+
let configError = null;
|
|
161
164
|
|
|
162
|
-
if (process.env.GX_CONFIG_PATH
|
|
165
|
+
if (process.env.GX_CONFIG_PATH) {
|
|
163
166
|
configSource = 'env';
|
|
164
|
-
} else if (
|
|
167
|
+
} else if (configFound) {
|
|
165
168
|
configSource = 'cwd';
|
|
166
169
|
}
|
|
167
170
|
|
|
168
|
-
if (
|
|
171
|
+
if (configFound) {
|
|
169
172
|
const cfg = readJsonFileSafe(configPath);
|
|
170
|
-
if (cfg) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
if (cfg && typeof cfg === 'object' && !Array.isArray(cfg)) {
|
|
174
|
+
configReadable = true;
|
|
175
|
+
const environment = cfg.Environment && typeof cfg.Environment === 'object' ? cfg.Environment : {};
|
|
176
|
+
kbCatalog = readKbCatalog(configPath, cfg);
|
|
177
|
+
const declaredKbs = Object.entries(kbCatalog.kbs || {});
|
|
178
|
+
const catalogPath = kbCatalog.activeKb
|
|
179
|
+
? kbCatalog.kbs[kbCatalog.activeKb] || null
|
|
180
|
+
: (declaredKbs.length === 1 ? declaredKbs[0][1] : null);
|
|
181
|
+
const legacyKbPath = typeof environment.KBPath === 'string' && environment.KBPath.trim()
|
|
182
|
+
? environment.KBPath
|
|
183
|
+
: null;
|
|
184
|
+
kbPath = legacyKbPath || catalogPath || null;
|
|
185
|
+
gxPath = cfg.GeneXus && typeof cfg.GeneXus.InstallationPath === 'string'
|
|
186
|
+
? cfg.GeneXus.InstallationPath
|
|
187
|
+
: null;
|
|
173
188
|
if (kbPath) kbLooksValid = directoryLooksLikeKnowledgeBase(kbPath);
|
|
189
|
+
} else {
|
|
190
|
+
configError = 'Configuration file is not a readable JSON object.';
|
|
174
191
|
}
|
|
192
|
+
} else if (configPath) {
|
|
193
|
+
configError = process.env.GX_CONFIG_PATH
|
|
194
|
+
? 'GX_CONFIG_PATH points to a missing configuration file.'
|
|
195
|
+
: 'Configuration file does not exist.';
|
|
175
196
|
}
|
|
176
197
|
|
|
177
198
|
const ready = configFound && gatewayExeFound;
|
|
178
|
-
return { ready, configFound, gatewayExeFound, kbLooksValid, configPath, gatewayExePath, kbPath, gxPath, configSource };
|
|
199
|
+
return { ready, configFound, configReadable, configError, gatewayExeFound, kbLooksValid, kbCatalog, configPath, gatewayExePath, kbPath, gxPath, configSource };
|
|
179
200
|
}
|
|
180
201
|
|
|
181
202
|
const PROBE_EXIT_GRACE_MS = 2000;
|
|
@@ -209,19 +230,116 @@ function stopProbeChild(child, graceMs = PROBE_EXIT_GRACE_MS) {
|
|
|
209
230
|
});
|
|
210
231
|
}
|
|
211
232
|
|
|
233
|
+
async function runGatewaySelfTest({ env = process.env, timeoutMs = 5000 }) {
|
|
234
|
+
const gatewayExePath = getGatewayExePath();
|
|
235
|
+
const maxOutputChars = 32 * 1024;
|
|
236
|
+
|
|
237
|
+
const appendOutput = (current, chunk) => {
|
|
238
|
+
const next = current + chunk.toString();
|
|
239
|
+
return next.length > maxOutputChars ? next.slice(-maxOutputChars) : next;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const summarize = (code, stdout, stderr) => {
|
|
243
|
+
let payload = null;
|
|
244
|
+
const lines = stdout.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
245
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
246
|
+
try {
|
|
247
|
+
payload = JSON.parse(lines[i]);
|
|
248
|
+
break;
|
|
249
|
+
} catch {
|
|
250
|
+
// Keep looking: a diagnostic line may precede the JSON envelope.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (payload && payload.schemaVersion === 'gateway-selftest/1' && Array.isArray(payload.checks)) {
|
|
255
|
+
const failed = payload.checks.find((check) => check.status === 'fail');
|
|
256
|
+
if (code === 0 && payload.ok !== false && !failed) {
|
|
257
|
+
return {
|
|
258
|
+
status: 'pass',
|
|
259
|
+
detail: `Gateway self-test passed (${payload.summary?.total || payload.checks.length} checks).`
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const reason = failed && failed.detail
|
|
263
|
+
? `: ${failed.id}: ${sanitizeOperationalMessage(failed.detail)}`
|
|
264
|
+
: ` (exit ${code === null ? 'unknown' : code})`;
|
|
265
|
+
return { status: 'fail', detail: `Gateway self-test failed${reason}.` };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const preview = sanitizeOperationalMessage((stderr || stdout).trim(), '');
|
|
269
|
+
return {
|
|
270
|
+
status: code === 0 ? 'warn' : 'fail',
|
|
271
|
+
detail: code === 0
|
|
272
|
+
? (preview ? `Gateway self-test returned no recognized result: ${preview}` : 'Gateway self-test returned no recognized result.')
|
|
273
|
+
: (preview
|
|
274
|
+
? `Gateway self-test failed (exit ${code === null ? 'unknown' : code}): ${preview}`
|
|
275
|
+
: `Gateway self-test failed (exit ${code === null ? 'unknown' : code}) with no diagnostic output.`)
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
return await new Promise((resolve) => {
|
|
280
|
+
let child;
|
|
281
|
+
let timer = null;
|
|
282
|
+
let settled = false;
|
|
283
|
+
let timedOut = false;
|
|
284
|
+
let stdout = '';
|
|
285
|
+
let stderr = '';
|
|
286
|
+
const finish = (result) => {
|
|
287
|
+
if (settled) return;
|
|
288
|
+
settled = true;
|
|
289
|
+
if (timer) clearTimeout(timer);
|
|
290
|
+
resolve(result);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
child = spawn(gatewayExePath, ['--self-test'], {
|
|
295
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
296
|
+
windowsHide: true,
|
|
297
|
+
env
|
|
298
|
+
});
|
|
299
|
+
child.stdout.on('data', (chunk) => { stdout = appendOutput(stdout, chunk); });
|
|
300
|
+
child.stderr.on('data', (chunk) => { stderr = appendOutput(stderr, chunk); });
|
|
301
|
+
child.once('error', (err) => {
|
|
302
|
+
finish({ status: 'fail', detail: `Gateway self-test could not start: ${sanitizeOperationalMessage(err.message)}` });
|
|
303
|
+
});
|
|
304
|
+
child.once('exit', (code) => {
|
|
305
|
+
if (timedOut) {
|
|
306
|
+
finish({ status: 'warn', detail: `Gateway self-test timed out after ${timeoutMs}ms.` });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
finish(summarize(code, stdout, stderr));
|
|
310
|
+
});
|
|
311
|
+
timer = setTimeout(async () => {
|
|
312
|
+
if (settled) return;
|
|
313
|
+
timedOut = true;
|
|
314
|
+
const exited = await stopProbeChild(child);
|
|
315
|
+
finish({
|
|
316
|
+
status: 'warn',
|
|
317
|
+
detail: exited
|
|
318
|
+
? `Gateway self-test timed out after ${timeoutMs}ms; process was stopped.`
|
|
319
|
+
: `Gateway self-test timed out after ${timeoutMs}ms and did not exit after the stop signal.`
|
|
320
|
+
});
|
|
321
|
+
}, timeoutMs);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
finish({ status: 'fail', detail: `Gateway self-test could not start: ${sanitizeOperationalMessage(err.message)}` });
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
212
328
|
async function spawnGatewayProbe({ env = process.env, spawnHoldMs, timeoutMs, label, successDetail }) {
|
|
213
329
|
const gatewayExePath = getGatewayExePath();
|
|
214
330
|
|
|
215
331
|
return await new Promise((resolve) => {
|
|
216
332
|
let done = false;
|
|
333
|
+
let timer = null;
|
|
217
334
|
const finish = (result) => {
|
|
218
335
|
if (done) return;
|
|
219
336
|
done = true;
|
|
337
|
+
if (timer) clearTimeout(timer);
|
|
220
338
|
resolve(result);
|
|
221
339
|
};
|
|
222
340
|
|
|
223
341
|
try {
|
|
224
|
-
const child = spawn(gatewayExePath, [
|
|
342
|
+
const child = spawn(gatewayExePath, [], {
|
|
225
343
|
stdio: 'ignore',
|
|
226
344
|
windowsHide: true,
|
|
227
345
|
env
|
|
@@ -241,6 +359,16 @@ async function spawnGatewayProbe({ env = process.env, spawnHoldMs, timeoutMs, la
|
|
|
241
359
|
}
|
|
242
360
|
});
|
|
243
361
|
|
|
362
|
+
child.once('exit', (code, signal) => {
|
|
363
|
+
if (done) return;
|
|
364
|
+
finish({
|
|
365
|
+
status: code === 0 ? 'warn' : 'fail',
|
|
366
|
+
detail: code === 0
|
|
367
|
+
? `${label} exited before the observation window (signal: ${signal || 'none'}).`
|
|
368
|
+
: `${label} exited with code ${code === null ? 'unknown' : code}${signal ? ` (signal: ${signal})` : ''}.`
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
244
372
|
child.once('spawn', () => {
|
|
245
373
|
setTimeout(async () => {
|
|
246
374
|
const exited = await stopProbeChild(child);
|
|
@@ -252,7 +380,7 @@ async function spawnGatewayProbe({ env = process.env, spawnHoldMs, timeoutMs, la
|
|
|
252
380
|
}, spawnHoldMs);
|
|
253
381
|
});
|
|
254
382
|
|
|
255
|
-
setTimeout(async () => {
|
|
383
|
+
timer = setTimeout(async () => {
|
|
256
384
|
if (done) return;
|
|
257
385
|
const exited = await stopProbeChild(child);
|
|
258
386
|
finish({
|
|
@@ -268,13 +396,11 @@ async function spawnGatewayProbe({ env = process.env, spawnHoldMs, timeoutMs, la
|
|
|
268
396
|
});
|
|
269
397
|
}
|
|
270
398
|
|
|
271
|
-
async function probeGatewaySpawn() {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
successDetail: 'Gateway process can be spawned (probe launched and terminated).'
|
|
277
|
-
});
|
|
399
|
+
async function probeGatewaySpawn({ configPath = null } = {}) {
|
|
400
|
+
const env = configPath
|
|
401
|
+
? { ...process.env, GX_CONFIG_PATH: configPath }
|
|
402
|
+
: process.env;
|
|
403
|
+
return runGatewaySelfTest({ env });
|
|
278
404
|
}
|
|
279
405
|
|
|
280
406
|
function resolveMcpSmokeTarget(cwd) {
|
|
@@ -284,9 +410,23 @@ function resolveMcpSmokeTarget(cwd) {
|
|
|
284
410
|
return { applicable: true, status: null, detail: null, baseUrl: fallback };
|
|
285
411
|
}
|
|
286
412
|
|
|
413
|
+
if (!fs.existsSync(configPath)) {
|
|
414
|
+
return {
|
|
415
|
+
applicable: false,
|
|
416
|
+
status: 'not_applicable',
|
|
417
|
+
detail: `MCP HTTP smoke skipped: resolved config file does not exist at ${configPath}.`,
|
|
418
|
+
baseUrl: null
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
287
422
|
const cfg = readJsonFileSafe(configPath);
|
|
288
|
-
if (!cfg || typeof cfg !== 'object') {
|
|
289
|
-
return {
|
|
423
|
+
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) {
|
|
424
|
+
return {
|
|
425
|
+
applicable: false,
|
|
426
|
+
status: 'not_applicable',
|
|
427
|
+
detail: `MCP HTTP smoke skipped: resolved config file is not a readable JSON object at ${configPath}.`,
|
|
428
|
+
baseUrl: null
|
|
429
|
+
};
|
|
290
430
|
}
|
|
291
431
|
|
|
292
432
|
const server = cfg.Server && typeof cfg.Server === 'object' ? cfg.Server : {};
|
|
@@ -437,9 +577,15 @@ async function handleStatus(options, ctx) {
|
|
|
437
577
|
};
|
|
438
578
|
}
|
|
439
579
|
|
|
440
|
-
function buildClientExeCrossCheck(packageExePath) {
|
|
580
|
+
function buildClientExeCrossCheck(packageExePath, clientRows = null) {
|
|
441
581
|
const packageNorm = normalizeExePath(packageExePath);
|
|
442
|
-
const
|
|
582
|
+
const gatewaySource = process.env.GENEXUS_MCP_GATEWAY_EXE
|
|
583
|
+
? 'GENEXUS_MCP_GATEWAY_EXE'
|
|
584
|
+
: 'packaged gateway (default)';
|
|
585
|
+
const gatewayTarget = `Configured Gateway: ${packageExePath} (source: ${gatewaySource})`;
|
|
586
|
+
const targets = clientRows
|
|
587
|
+
? clientRows.map((row) => ({ name: row.name, path: row.configPath, entry: row.command ? { command: row.command } : null }))
|
|
588
|
+
: filterClientTargets(getClientConfigTargets(), { platform: process.platform });
|
|
443
589
|
|
|
444
590
|
const mismatches = [];
|
|
445
591
|
const matches = [];
|
|
@@ -447,13 +593,17 @@ function buildClientExeCrossCheck(packageExePath) {
|
|
|
447
593
|
let inspected = 0;
|
|
448
594
|
|
|
449
595
|
for (const client of targets) {
|
|
450
|
-
if (!fs.existsSync(client.path)) continue;
|
|
451
596
|
let entry;
|
|
452
|
-
|
|
453
|
-
entry =
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
597
|
+
if (clientRows) {
|
|
598
|
+
entry = client.entry;
|
|
599
|
+
} else {
|
|
600
|
+
if (!fs.existsSync(client.path)) continue;
|
|
601
|
+
try {
|
|
602
|
+
entry = readClientCommandEntry(client);
|
|
603
|
+
} catch (err) {
|
|
604
|
+
errors.push(`${client.name}: ${err.message || 'read failed'}`);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
457
607
|
}
|
|
458
608
|
if (!entry || !entry.command) continue;
|
|
459
609
|
inspected += 1;
|
|
@@ -489,14 +639,14 @@ function buildClientExeCrossCheck(packageExePath) {
|
|
|
489
639
|
if (mismatches.length === 0) {
|
|
490
640
|
return {
|
|
491
641
|
status: 'pass',
|
|
492
|
-
detail: `All inspected client configs (${matches.join(', ')})
|
|
642
|
+
detail: `All inspected client configs (${matches.join(', ')}) match the configured gateway target. ${gatewayTarget}.`
|
|
493
643
|
};
|
|
494
644
|
}
|
|
495
645
|
|
|
496
646
|
const detailParts = mismatches.map((m) => `${m.client} -> ${m.configured}${m.exists ? '' : ' (missing)'}`);
|
|
497
647
|
return {
|
|
498
648
|
status: 'warn',
|
|
499
|
-
detail: `Client(s) reference a gateway exe that is
|
|
649
|
+
detail: `Client(s) reference a gateway exe that is not the configured target. ${gatewayTarget}. \`npm install -g genexus-mcp@latest\` will NOT update those instances. Mismatches: ${detailParts.join('; ')}. Re-run scripts/install.ps1 (or genexus-mcp init --write-clients) to resync.`
|
|
500
650
|
};
|
|
501
651
|
}
|
|
502
652
|
|
|
@@ -603,25 +753,37 @@ function buildInProcessBuildAssemblyLoadCheck(gxPath) {
|
|
|
603
753
|
};
|
|
604
754
|
}
|
|
605
755
|
|
|
606
|
-
function buildGxPublicComCheck(gxMajor
|
|
756
|
+
function buildGxPublicComCheck(gxMajor) {
|
|
607
757
|
if (gxMajor !== '8' && gxMajor !== '9') {
|
|
608
758
|
return { id: 'gxpublic_com_registration', status: 'not_applicable', detail: 'Not a GeneXus 8.0/9.0 installation.' };
|
|
609
759
|
}
|
|
610
760
|
const child_process = require('child_process');
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
761
|
+
const providers = ['GXPublic.GXPublic.4', 'GXPublic.GXPublic.5', 'GXPubGXX.GXPublic.5', 'GXPubGXX.GXPublic'];
|
|
762
|
+
const registered = [];
|
|
763
|
+
for (const provider of providers) {
|
|
764
|
+
try {
|
|
765
|
+
const res = child_process.spawnSync('reg', ['query', `HKCR\\${provider}`], { encoding: 'utf8', timeout: 2000 });
|
|
766
|
+
if (res.status === 0) registered.push(provider);
|
|
767
|
+
} catch { }
|
|
768
|
+
}
|
|
616
769
|
|
|
617
|
-
if (
|
|
618
|
-
|
|
770
|
+
if (registered.length > 0) {
|
|
771
|
+
const matching = gxMajor === '8'
|
|
772
|
+
? ['GXPublic.GXPublic.4', 'GXPubGXX.GXPublic.5', 'GXPubGXX.GXPublic']
|
|
773
|
+
: ['GXPublic.GXPublic.5', 'GXPubGXX.GXPublic.5', 'GXPubGXX.GXPublic'];
|
|
774
|
+
const hasMatchingProvider = registered.some((provider) => matching.includes(provider));
|
|
775
|
+
return {
|
|
776
|
+
id: 'gxpublic_com_registration',
|
|
777
|
+
status: hasMatchingProvider ? 'pass' : 'warn',
|
|
778
|
+
detail: hasMatchingProvider
|
|
779
|
+
? `GXPublic OLE DB provider(s) registered for the 32-bit Worker: ${registered.join(', ')}.`
|
|
780
|
+
: `GXPublic provider(s) registered for the 32-bit Worker: ${registered.join(', ')}, but none is the documented GeneXus ${gxMajor}.0 provider (${matching.join(', ')}). Install the matching version before opening this KB.`
|
|
781
|
+
};
|
|
619
782
|
}
|
|
620
|
-
const dllHint = gxPath ? path.join(gxPath, 'GxPublic.dll') : 'GxPublic.dll';
|
|
621
783
|
return {
|
|
622
784
|
id: 'gxpublic_com_registration',
|
|
623
785
|
status: 'warn',
|
|
624
|
-
detail: `GXPublic
|
|
786
|
+
detail: `No supported GXPublic OLE DB provider is registered for GeneXus ${gxMajor}.0. Install the versioned GXPublic provider matching the KB generation (GXPublic 8.0 for GX8 or GXPublic Yi for GX9).`
|
|
625
787
|
};
|
|
626
788
|
}
|
|
627
789
|
|
|
@@ -669,7 +831,21 @@ function redactConfig(cfg) {
|
|
|
669
831
|
return walk(cfg);
|
|
670
832
|
}
|
|
671
833
|
|
|
672
|
-
|
|
834
|
+
function redactDiagnosticText(value, paths) {
|
|
835
|
+
let text = String(value ?? '');
|
|
836
|
+
const candidates = [...new Set((paths || []).filter((candidate) => typeof candidate === 'string' && candidate.length > 0))]
|
|
837
|
+
.sort((a, b) => b.length - a.length);
|
|
838
|
+
for (const candidate of candidates) {
|
|
839
|
+
const token = `<redacted:${require('crypto').createHash('sha256').update(candidate).digest('hex').slice(0, 8)}>`;
|
|
840
|
+
for (const variant of new Set([candidate, candidate.replace(/\\/g, '/')])) {
|
|
841
|
+
const escaped = variant.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
842
|
+
text = text.replace(new RegExp(escaped, 'gi'), token);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return text;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function buildSupportDump({ checks, summary, data, gatewayExePath, clientRows = [], toolDefPath, stdioErrorLogPath, ctx }) {
|
|
673
849
|
const os = require('os');
|
|
674
850
|
const crypto = require('crypto');
|
|
675
851
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
@@ -685,7 +861,23 @@ async function buildSupportDump({ checks, summary, data, gatewayExePath, ctx })
|
|
|
685
861
|
entries.push(relPath);
|
|
686
862
|
};
|
|
687
863
|
|
|
688
|
-
|
|
864
|
+
const redactionPaths = [
|
|
865
|
+
gatewayExePath,
|
|
866
|
+
data.configPath,
|
|
867
|
+
data.kbPath,
|
|
868
|
+
data.gxPath,
|
|
869
|
+
toolDefPath,
|
|
870
|
+
stdioErrorLogPath,
|
|
871
|
+
...Object.values(data.kbCatalog?.kbs || {}),
|
|
872
|
+
...clientRows.flatMap((row) => [row.configPath, row.command])
|
|
873
|
+
];
|
|
874
|
+
const redactedChecks = checks.map((check) => ({
|
|
875
|
+
...check,
|
|
876
|
+
detail: redactDiagnosticText(check.detail, redactionPaths)
|
|
877
|
+
}));
|
|
878
|
+
const redactPath = (value) => redactDiagnosticText(value, redactionPaths);
|
|
879
|
+
|
|
880
|
+
writeEntry('doctor.json', { summary, checks: redactedChecks, generatedAt: new Date().toISOString() });
|
|
689
881
|
|
|
690
882
|
if (data.configPath && fs.existsSync(data.configPath)) {
|
|
691
883
|
const cfg = readJsonFileSafe(data.configPath);
|
|
@@ -701,7 +893,7 @@ async function buildSupportDump({ checks, summary, data, gatewayExePath, ctx })
|
|
|
701
893
|
nodeVersion: process.version,
|
|
702
894
|
osRelease: os.release(),
|
|
703
895
|
cwdHash: crypto.createHash('sha256').update(ctx.cwd || '').digest('hex').slice(0, 8),
|
|
704
|
-
gatewayExePath,
|
|
896
|
+
gatewayExePath: redactPath(gatewayExePath),
|
|
705
897
|
gatewayExeExists: fs.existsSync(gatewayExePath),
|
|
706
898
|
configSource: data.configSource,
|
|
707
899
|
kbConfigured: !!data.kbPath,
|
|
@@ -783,27 +975,38 @@ async function handleDoctor(options, ctx) {
|
|
|
783
975
|
const stdioErrorLogExists = !!(stdioErrorLogPath && fs.existsSync(stdioErrorLogPath));
|
|
784
976
|
|
|
785
977
|
let toolCount = 0;
|
|
978
|
+
let toolDefinitionsValid = false;
|
|
979
|
+
let toolDefinitionsError = null;
|
|
786
980
|
if (toolDefsExists) {
|
|
787
981
|
try {
|
|
788
982
|
const parsed = JSON.parse(fs.readFileSync(toolDefPath, 'utf8'));
|
|
789
|
-
if (Array.isArray(parsed))
|
|
790
|
-
|
|
791
|
-
|
|
983
|
+
if (Array.isArray(parsed)) {
|
|
984
|
+
toolDefinitionsValid = true;
|
|
985
|
+
toolCount = parsed.length;
|
|
986
|
+
} else {
|
|
987
|
+
toolDefinitionsError = 'tool_definitions.json is not an array.';
|
|
988
|
+
}
|
|
989
|
+
} catch (err) {
|
|
990
|
+
toolDefinitionsError = sanitizeOperationalMessage(err && err.message ? err.message : 'invalid JSON');
|
|
792
991
|
}
|
|
793
992
|
}
|
|
794
993
|
|
|
795
994
|
const kbPath = data.kbPath;
|
|
796
995
|
const gxPath = data.gxPath;
|
|
797
996
|
const kbExists = !!(kbPath && fs.existsSync(kbPath));
|
|
798
|
-
const
|
|
997
|
+
const gxExecutable = gxPath && ['genexus.exe', 'gx.exe', 'gxw32.exe']
|
|
998
|
+
.find((name) => fs.existsSync(path.join(gxPath, name)));
|
|
999
|
+
const gxExeExists = !!gxExecutable;
|
|
799
1000
|
const kbSdkCompatibility = kbExists && gxExeExists
|
|
800
1001
|
? compareGeneXusKbAndInstallation(kbPath, gxPath)
|
|
801
1002
|
: null;
|
|
802
1003
|
const catalog = getGeneXusVersionCatalog();
|
|
803
1004
|
|
|
804
1005
|
let unsupportedCompatibilityMajor = null;
|
|
805
|
-
let compatibilityStatus = 'warn';
|
|
806
|
-
let compatibilityDetail =
|
|
1006
|
+
let compatibilityStatus = kbPath && gxPath ? 'warn' : 'not_applicable';
|
|
1007
|
+
let compatibilityDetail = kbPath && gxPath
|
|
1008
|
+
? 'KB/SDK major compatibility check was skipped until both configured paths exist.'
|
|
1009
|
+
: 'KB/SDK major compatibility check is not applicable until both paths are configured.';
|
|
807
1010
|
if (kbSdkCompatibility) {
|
|
808
1011
|
if (!isSupportedCatalogMajor(catalog, kbSdkCompatibility.kb.major)) {
|
|
809
1012
|
unsupportedCompatibilityMajor = `KB major ${kbSdkCompatibility.kb.major}`;
|
|
@@ -829,10 +1032,48 @@ async function handleDoctor(options, ctx) {
|
|
|
829
1032
|
const gxVersionLabel = gxIdent && gxIdent.version ? ` (GeneXus ${gxIdent.major || ''} version ${gxIdent.version})` : (gxIdent && gxIdent.major ? ` (GeneXus ${gxIdent.major})` : '');
|
|
830
1033
|
|
|
831
1034
|
const riskyZone = isPathLikelyAppLockerBlocked(gatewayExePath);
|
|
832
|
-
const
|
|
1035
|
+
const clientRows = clientsStatus();
|
|
1036
|
+
const clientCrossCheck = buildClientExeCrossCheck(gatewayExePath, clientRows);
|
|
1037
|
+
const configCheck = data.configFound
|
|
1038
|
+
? (data.configReadable
|
|
1039
|
+
? { status: 'pass', detail: `GX config file was found at ${data.configPath} (source: ${data.configSource}).` }
|
|
1040
|
+
: { status: 'fail', detail: `GX config file could not be parsed as a JSON object: ${data.configPath}.` })
|
|
1041
|
+
: {
|
|
1042
|
+
status: 'fail',
|
|
1043
|
+
detail: data.configPath
|
|
1044
|
+
? `${data.configError || 'GX config file is missing.'} ${data.configPath}`
|
|
1045
|
+
: 'GX config file is missing.'
|
|
1046
|
+
};
|
|
1047
|
+
const toolDefinitionsCheck = !toolDefsExists
|
|
1048
|
+
? {
|
|
1049
|
+
status: 'warn',
|
|
1050
|
+
detail: process.env.GENEXUS_MCP_TOOL_DEFINITIONS
|
|
1051
|
+
? `tool_definitions.json missing at GENEXUS_MCP_TOOL_DEFINITIONS=${toolDefPath}. Unset the env var or point it at a valid file.`
|
|
1052
|
+
: `tool_definitions.json missing. Expected at ${toolDefPath} (next to the gateway exe). The csproj should copy it on publish — reinstall via scripts/install.ps1, or set GENEXUS_MCP_TOOL_DEFINITIONS to override.`
|
|
1053
|
+
}
|
|
1054
|
+
: toolDefinitionsValid
|
|
1055
|
+
? { status: 'pass', detail: `Tool definition file found (${toolCount} tools) at ${toolDefPath}.` }
|
|
1056
|
+
: { status: 'fail', detail: `tool_definitions.json is invalid at ${toolDefPath}${toolDefinitionsError ? `: ${toolDefinitionsError}` : '.'}` };
|
|
1057
|
+
const gxEnvCheck = process.env.GX_CONFIG_PATH
|
|
1058
|
+
? { status: 'pass', detail: 'GX_CONFIG_PATH env var is set.' }
|
|
1059
|
+
: data.configFound
|
|
1060
|
+
? { status: 'not_applicable', detail: `GX_CONFIG_PATH is not set; using ${data.configPath} from the current directory.` }
|
|
1061
|
+
: { status: 'warn', detail: 'GX_CONFIG_PATH env var is not set and no config file was found.' };
|
|
1062
|
+
const kbCatalogEntries = Object.entries(data.kbCatalog?.kbs || {});
|
|
1063
|
+
const missingCatalogKbs = kbCatalogEntries.filter(([, declaredPath]) => !fs.existsSync(declaredPath));
|
|
1064
|
+
const selectedCatalogAlias = data.kbCatalog?.activeKb || (kbCatalogEntries.length === 1 ? kbCatalogEntries[0][0] : null);
|
|
1065
|
+
const kbCatalogCheck = kbCatalogEntries.length === 0
|
|
1066
|
+
? { status: 'not_applicable', detail: 'No KB aliases are declared in Environment.KBs.' }
|
|
1067
|
+
: missingCatalogKbs.length > 0
|
|
1068
|
+
? { status: 'fail', detail: `Declared KB path(s) missing: ${missingCatalogKbs.map(([alias, declaredPath]) => `${alias} -> ${declaredPath}`).join('; ')}.` }
|
|
1069
|
+
: data.kbCatalog.activeKb && !data.kbCatalog.kbs[data.kbCatalog.activeKb]
|
|
1070
|
+
? { status: 'fail', detail: `Active/Default KB alias '${data.kbCatalog.activeKb}' is not declared in Environment.KBs.` }
|
|
1071
|
+
: kbCatalogEntries.length > 1 && !data.kbCatalog.activeKb
|
|
1072
|
+
? { status: 'warn', detail: `${kbCatalogEntries.length} KB aliases are declared but neither ActiveKb nor DefaultKb selects one. Strict sessions must use an explicit kb.` }
|
|
1073
|
+
: { status: 'pass', detail: `${kbCatalogEntries.length} declared KB alias(es) are present${selectedCatalogAlias ? `; diagnostic target: ${selectedCatalogAlias}` : ''}.` };
|
|
833
1074
|
|
|
834
1075
|
const checks = [
|
|
835
|
-
{ id: 'config_file', status:
|
|
1076
|
+
{ id: 'config_file', status: configCheck.status, detail: configCheck.detail },
|
|
836
1077
|
{ id: 'gateway_exe', status: data.gatewayExeFound ? 'pass' : 'fail', detail: data.gatewayExeFound ? 'Gateway executable is available.' : 'Gateway executable is missing.' },
|
|
837
1078
|
{
|
|
838
1079
|
id: 'stdio_error_log',
|
|
@@ -851,18 +1092,19 @@ async function handleDoctor(options, ctx) {
|
|
|
851
1092
|
// A KB path configured but absent on disk is fatal — the worker can't open a KB
|
|
852
1093
|
// that doesn't exist. Only when no KB is configured at all do we soften to warn.
|
|
853
1094
|
{ id: 'kb_path_exists', status: kbExists ? 'pass' : (kbPath ? 'fail' : 'warn'), detail: kbExists ? 'Configured KB path exists.' : (kbPath ? `Configured KB path does not exist: ${kbPath}` : 'No KB path is configured.') },
|
|
1095
|
+
{ id: 'kb_catalog', status: kbCatalogCheck.status, detail: kbCatalogCheck.detail },
|
|
854
1096
|
{ id: 'kb_shape', status: data.kbLooksValid ? 'pass' : 'warn', detail: data.kbLooksValid ? 'KB folder shape looks valid.' : 'KB markers were not found in configured KB path.' },
|
|
855
|
-
// Same logic for the GeneXus install: missing
|
|
1097
|
+
// Same logic for the GeneXus install: missing the configured executable at a path
|
|
856
1098
|
// guarantees a worker crash on first MCP call. Promote from warn to fail so init
|
|
857
1099
|
// exits non-zero and the caller (install.ps1, AI client) actually sees the problem.
|
|
858
|
-
{ id: 'gx_installation', status: gxExeExists ? 'pass' : (gxPath ? 'fail' : 'warn'), detail: gxExeExists ? `GeneXus installation has ${
|
|
1100
|
+
{ id: 'gx_installation', status: gxExeExists ? 'pass' : (gxPath ? 'fail' : 'warn'), detail: gxExeExists ? `GeneXus installation has ${gxExecutable}${gxVersionLabel}.` : (gxPath ? `Configured GeneXus installation is missing executable at: ${gxPath}` : 'No GeneXus installation path is configured.') },
|
|
859
1101
|
{
|
|
860
1102
|
id: 'kb_sdk_compatibility',
|
|
861
1103
|
status: compatibilityStatus,
|
|
862
1104
|
detail: compatibilityDetail
|
|
863
1105
|
},
|
|
864
|
-
{ id: 'tool_definitions', status:
|
|
865
|
-
{ id: 'gx_env', status:
|
|
1106
|
+
{ id: 'tool_definitions', status: toolDefinitionsCheck.status, detail: toolDefinitionsCheck.detail },
|
|
1107
|
+
{ id: 'gx_env', status: gxEnvCheck.status, detail: gxEnvCheck.detail },
|
|
866
1108
|
{ id: 'client_config_sync', status: clientCrossCheck.status, detail: clientCrossCheck.detail }
|
|
867
1109
|
];
|
|
868
1110
|
|
|
@@ -880,7 +1122,6 @@ async function handleDoctor(options, ctx) {
|
|
|
880
1122
|
checks.push({ id: 'legacy_ide_lock', status: ideLockCheck.status, detail: ideLockCheck.detail });
|
|
881
1123
|
|
|
882
1124
|
// Client registration summary — one line answering "are my AI agents wired up?".
|
|
883
|
-
const clientRows = clientsStatus();
|
|
884
1125
|
const installedRows = clientRows.filter((r) => r.installed);
|
|
885
1126
|
const staleRows = clientRows.filter((r) => r.commandStale);
|
|
886
1127
|
const installedUnregistered = installedRows.filter((r) => !r.registered && r.writeSupported);
|
|
@@ -898,11 +1139,13 @@ async function handleDoctor(options, ctx) {
|
|
|
898
1139
|
}
|
|
899
1140
|
checks.push({ id: 'clients_registered', status: clientsStatusLevel, detail: clientsDetail });
|
|
900
1141
|
|
|
901
|
-
if (data.gatewayExeFound) {
|
|
902
|
-
const probe = await probeGatewaySpawn();
|
|
1142
|
+
if (data.gatewayExeFound && data.configFound && data.configReadable) {
|
|
1143
|
+
const probe = await probeGatewaySpawn({ configPath: data.configPath });
|
|
903
1144
|
checks.push({ id: 'gateway_spawn_probe', status: probe.status, detail: probe.detail });
|
|
904
|
-
} else {
|
|
1145
|
+
} else if (!data.gatewayExeFound) {
|
|
905
1146
|
checks.push({ id: 'gateway_spawn_probe', status: 'warn', detail: 'Spawn probe skipped: gateway exe not found.' });
|
|
1147
|
+
} else {
|
|
1148
|
+
checks.push({ id: 'gateway_spawn_probe', status: 'not_applicable', detail: 'Gateway self-test skipped: no usable configuration was resolved.' });
|
|
906
1149
|
}
|
|
907
1150
|
|
|
908
1151
|
if (options.mcpSmoke) {
|
|
@@ -921,7 +1164,7 @@ async function handleDoctor(options, ctx) {
|
|
|
921
1164
|
// to send 5 separate things over chat.
|
|
922
1165
|
if (options.dump) {
|
|
923
1166
|
try {
|
|
924
|
-
const dumpResult = await buildSupportDump({ checks, summary, data, gatewayExePath, ctx });
|
|
1167
|
+
const dumpResult = await buildSupportDump({ checks, summary, data, gatewayExePath, clientRows, toolDefPath, stdioErrorLogPath, ctx });
|
|
925
1168
|
return {
|
|
926
1169
|
exitCode: ctx.EXIT_CODES.OK,
|
|
927
1170
|
envelope: {
|
|
@@ -932,7 +1175,7 @@ async function handleDoctor(options, ctx) {
|
|
|
932
1175
|
entries: dumpResult.entries,
|
|
933
1176
|
summary
|
|
934
1177
|
},
|
|
935
|
-
help: ['Attach the zip to your support ticket. Paths
|
|
1178
|
+
help: ['Attach the zip to your support ticket. Paths in config.json and doctor metadata have been redacted; sensitive values may still appear in worker logs — review before sharing if needed.']
|
|
936
1179
|
}
|
|
937
1180
|
};
|
|
938
1181
|
} catch (err) {
|
|
@@ -1506,7 +1749,9 @@ async function runInteractiveInit(ctx) {
|
|
|
1506
1749
|
};
|
|
1507
1750
|
}
|
|
1508
1751
|
|
|
1509
|
-
|
|
1752
|
+
const gxExecutable = finalGx && ['genexus.exe', 'gx.exe', 'gxw32.exe']
|
|
1753
|
+
.find((name) => fs.existsSync(path.join(finalGx, name)));
|
|
1754
|
+
if (!gxExecutable) {
|
|
1510
1755
|
const suggested = discoverGeneXusInstallation(kbIdentity.major);
|
|
1511
1756
|
const help = [`Path checked: ${finalGx}`];
|
|
1512
1757
|
if (suggested && suggested.toLowerCase() !== finalGx.toLowerCase()) {
|
|
@@ -1516,7 +1761,7 @@ async function runInteractiveInit(ctx) {
|
|
|
1516
1761
|
return {
|
|
1517
1762
|
exitCode: ctx.EXIT_CODES.ERROR,
|
|
1518
1763
|
envelope: operationalErrorEnvelope(
|
|
1519
|
-
`GeneXus path does not contain genexus.exe. Aborted before writing config to avoid silent worker crashes.`,
|
|
1764
|
+
`GeneXus path does not contain a supported executable (genexus.exe, gx.exe, or gxw32.exe). Aborted before writing config to avoid silent worker crashes.`,
|
|
1520
1765
|
ctx.EXIT_CODES.ERROR,
|
|
1521
1766
|
help
|
|
1522
1767
|
)
|
|
@@ -1725,7 +1970,9 @@ async function handleInit(options, ctx) {
|
|
|
1725
1970
|
// install (e.g. C:\...\GeneXus18u7 vs the GeneXus18 default) used to slip through
|
|
1726
1971
|
// — init wrote the config, then the worker crashed on first MCP call with no
|
|
1727
1972
|
// useful signal back to the operator.
|
|
1728
|
-
|
|
1973
|
+
const gxExecutable = resolution.gx.value && ['genexus.exe', 'gx.exe', 'gxw32.exe']
|
|
1974
|
+
.find((name) => fs.existsSync(path.join(resolution.gx.value, name)));
|
|
1975
|
+
if (!gxExecutable) {
|
|
1729
1976
|
const help = [
|
|
1730
1977
|
`Path checked: ${resolution.gx.value}`,
|
|
1731
1978
|
`Source: --${resolution.gx.source === 'flag' ? 'gx flag' : resolution.gx.source}`
|
|
@@ -1742,7 +1989,7 @@ async function handleInit(options, ctx) {
|
|
|
1742
1989
|
return {
|
|
1743
1990
|
exitCode: ctx.EXIT_CODES.ERROR,
|
|
1744
1991
|
envelope: operationalErrorEnvelope(
|
|
1745
|
-
`Configured GeneXus path does not contain genexus.exe. Init aborted before writing config to avoid silent worker crashes.`,
|
|
1992
|
+
`Configured GeneXus path does not contain a supported executable (genexus.exe, gx.exe, or gxw32.exe). Init aborted before writing config to avoid silent worker crashes.`,
|
|
1746
1993
|
ctx.EXIT_CODES.ERROR,
|
|
1747
1994
|
help
|
|
1748
1995
|
)
|