genexus-mcp 3.2.1 → 3.2.3
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 +7 -6
- package/cli/commands/axi.js +116 -7
- package/cli/docs.test.js +29 -0
- package/cli/index.js +57 -6
- package/cli/lib/client-adapters.test.js +183 -3
- package/cli/lib/config.js +203 -61
- package/cli/run.test.js +265 -5
- package/docs/llm_cli_mcp_playbook.md +39 -0
- 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.json +8 -6
- package/publish/gxmcp-manifest.json +12 -12
- package/publish/gxmcp-sbom.json +4 -4
- package/publish/nexus-ide.vsix +0 -0
- package/publish/tool_definitions.json +9 -5
- package/publish/worker/GxMcp.Worker.exe +0 -0
- package/publish/worker/sdk-compatibility.json +24 -0
package/cli/lib/config.js
CHANGED
|
@@ -22,6 +22,28 @@ function generateConfig(gxPath, kbPath) {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function generateNeutralConfig(gxPath, { workerPath, gatewayMode = 'stdio-isolated', resolutionPolicy = 'strict' } = {}) {
|
|
26
|
+
return {
|
|
27
|
+
ConfigSchemaVersion: 2,
|
|
28
|
+
GatewayMode: gatewayMode,
|
|
29
|
+
GeneXus: {
|
|
30
|
+
InstallationPath: gxPath,
|
|
31
|
+
WorkerExecutable: workerPath || path.join(path.dirname(getGatewayExePath()), 'worker', 'GxMcp.Worker.exe')
|
|
32
|
+
},
|
|
33
|
+
Server: {
|
|
34
|
+
HttpPort: 0,
|
|
35
|
+
McpStdio: true,
|
|
36
|
+
SessionIdleTimeoutMinutes: 10,
|
|
37
|
+
WorkerIdleTimeoutMinutes: 5,
|
|
38
|
+
EmitStructuredContent: false,
|
|
39
|
+
TerseResponses: true
|
|
40
|
+
},
|
|
41
|
+
Environment: {
|
|
42
|
+
ResolutionPolicy: resolutionPolicy
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
function getGatewayExePath() {
|
|
26
48
|
if (process.env.GENEXUS_MCP_GATEWAY_EXE) {
|
|
27
49
|
return process.env.GENEXUS_MCP_GATEWAY_EXE;
|
|
@@ -381,9 +403,9 @@ function stripJsonComments(text) {
|
|
|
381
403
|
return out;
|
|
382
404
|
}
|
|
383
405
|
|
|
384
|
-
function readJsonFileSafe(filePath) {
|
|
406
|
+
function readJsonFileSafe(filePath, fileSystem = fs) {
|
|
385
407
|
try {
|
|
386
|
-
const raw =
|
|
408
|
+
const raw = fileSystem.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
|
|
387
409
|
if (!raw.trim()) return {};
|
|
388
410
|
try {
|
|
389
411
|
return JSON.parse(raw);
|
|
@@ -406,69 +428,151 @@ function readJsonFileSafe(filePath) {
|
|
|
406
428
|
|
|
407
429
|
// Atomic write: stage to a temp file then rename over the target, so a crash
|
|
408
430
|
// mid-write can never leave a client's config truncated.
|
|
409
|
-
function writeFileAtomic(filePath, content) {
|
|
431
|
+
function writeFileAtomic(filePath, content, fileSystem = fs) {
|
|
410
432
|
const tmp = `${filePath}.tmp-${process.pid}`;
|
|
411
|
-
|
|
433
|
+
fileSystem.writeFileSync(tmp, content);
|
|
412
434
|
try {
|
|
413
|
-
|
|
435
|
+
fileSystem.renameSync(tmp, filePath);
|
|
414
436
|
} catch (err) {
|
|
415
|
-
try {
|
|
437
|
+
try { fileSystem.rmSync(tmp, { force: true }); } catch { /* ignore */ }
|
|
416
438
|
throw err;
|
|
417
439
|
}
|
|
418
440
|
}
|
|
419
441
|
|
|
420
442
|
// Back up a client config once per process run before the first mutation, so the
|
|
421
443
|
// user has a restore point (the old build-from-source install.ps1 did this; the
|
|
422
|
-
// CLI now owns it).
|
|
444
|
+
// CLI now owns it). A failed backup blocks the mutation so the result remains
|
|
445
|
+
// recoverable.
|
|
423
446
|
// After writing a new backup, prune old .bak files for the same config so at
|
|
424
447
|
// most BAK_KEEP_COUNT backups exist (oldest removed first).
|
|
425
448
|
const BAK_KEEP_COUNT = 5;
|
|
426
449
|
const _backedUpThisRun = new Set();
|
|
427
|
-
function backupClientConfigOnce(filePath) {
|
|
428
|
-
if (!
|
|
450
|
+
function backupClientConfigOnce(filePath, fileSystem = fs) {
|
|
451
|
+
if (!fileSystem.existsSync(filePath)) return null;
|
|
429
452
|
// Case-fold the dedupe key only on Windows; lowercasing on a case-sensitive
|
|
430
453
|
// filesystem could merge two genuinely distinct paths.
|
|
431
454
|
const resolved = path.resolve(filePath);
|
|
432
455
|
const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
433
456
|
if (_backedUpThisRun.has(key)) return null;
|
|
434
|
-
|
|
435
|
-
const d = new Date();
|
|
457
|
+
const d = new Date();
|
|
436
458
|
const stamp = d.toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
437
|
-
|
|
438
|
-
|
|
459
|
+
let bak = `${filePath}.${stamp}.bak`;
|
|
460
|
+
let suffix = 1;
|
|
461
|
+
while (fileSystem.existsSync(bak)) bak = `${filePath}.${stamp}-${suffix++}.bak`;
|
|
462
|
+
fileSystem.copyFileSync(filePath, bak);
|
|
439
463
|
_backedUpThisRun.add(key);
|
|
440
464
|
// Prune: keep only the BAK_KEEP_COUNT most-recent .bak files for this config.
|
|
441
465
|
try {
|
|
442
466
|
const dir = path.dirname(resolved);
|
|
443
467
|
const base = path.basename(resolved);
|
|
444
468
|
const bakPattern = new RegExp(`^${base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.\\d{14}\\.bak$`);
|
|
445
|
-
const existing =
|
|
469
|
+
const existing = fileSystem.readdirSync(dir)
|
|
446
470
|
.filter(f => bakPattern.test(f))
|
|
447
471
|
.map(f => path.join(dir, f))
|
|
448
472
|
.sort(); // ISO timestamp stamps sort lexicographically = chronologically
|
|
449
473
|
if (existing.length > BAK_KEEP_COUNT) {
|
|
450
474
|
const toRemove = existing.slice(0, existing.length - BAK_KEEP_COUNT);
|
|
451
475
|
for (const old of toRemove) {
|
|
452
|
-
try {
|
|
476
|
+
try { fileSystem.rmSync(old, { force: true }); } catch { /* best-effort */ }
|
|
453
477
|
}
|
|
454
478
|
}
|
|
455
479
|
} catch { /* pruning is best-effort; never block the backup */ }
|
|
456
|
-
|
|
457
|
-
} catch {
|
|
458
|
-
return null;
|
|
459
|
-
}
|
|
480
|
+
return bak;
|
|
460
481
|
}
|
|
461
482
|
|
|
462
483
|
// Write JSON to a client config: back up, serialize, write atomically.
|
|
463
|
-
function writeClientJson(filePath, obj) {
|
|
464
|
-
backupClientConfigOnce(filePath);
|
|
465
|
-
writeFileAtomic(filePath, JSON.stringify(obj, null, 2));
|
|
484
|
+
function writeClientJson(filePath, obj, fileSystem = fs) {
|
|
485
|
+
backupClientConfigOnce(filePath, fileSystem);
|
|
486
|
+
writeFileAtomic(filePath, JSON.stringify(obj, null, 2), fileSystem);
|
|
466
487
|
}
|
|
467
488
|
|
|
468
489
|
// Write raw text to a client config (e.g. Codex TOML): back up + write atomically.
|
|
469
|
-
function writeClientText(filePath, content) {
|
|
470
|
-
backupClientConfigOnce(filePath);
|
|
471
|
-
writeFileAtomic(filePath, content);
|
|
490
|
+
function writeClientText(filePath, content, fileSystem = fs) {
|
|
491
|
+
backupClientConfigOnce(filePath, fileSystem);
|
|
492
|
+
writeFileAtomic(filePath, content, fileSystem);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function copyFileAtomic(sourcePath, targetPath, fileSystem = fs) {
|
|
496
|
+
const tmp = `${targetPath}.tmp-${process.pid}`;
|
|
497
|
+
fileSystem.copyFileSync(sourcePath, tmp);
|
|
498
|
+
try {
|
|
499
|
+
fileSystem.renameSync(tmp, targetPath);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
try { fileSystem.rmSync(tmp, { force: true }); } catch { /* ignore */ }
|
|
502
|
+
throw err;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function migrateLegacyConfig(sourcePath, targetPath, { rejectNonMigratable = false, fileSystem = fs } = {}) {
|
|
507
|
+
const source = path.resolve(sourcePath);
|
|
508
|
+
const target = path.resolve(targetPath);
|
|
509
|
+
const legacy = readJsonFileSafe(source, fileSystem);
|
|
510
|
+
if (!legacy || typeof legacy !== 'object') {
|
|
511
|
+
const err = new Error(`Legacy config is missing or invalid: ${source}`);
|
|
512
|
+
err.code = 'INVALID_CONFIG';
|
|
513
|
+
throw err;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const env = legacy.Environment && typeof legacy.Environment === 'object' ? legacy.Environment : {};
|
|
517
|
+
const notMigrated = ['KBPath', 'KBs', 'DefaultKb', 'ActiveKb']
|
|
518
|
+
.filter((key) => Object.prototype.hasOwnProperty.call(env, key))
|
|
519
|
+
.map((key) => `Environment.${key}`);
|
|
520
|
+
if (rejectNonMigratable && notMigrated.length > 0) {
|
|
521
|
+
const err = new Error(`Migration rejected non-migratable fields: ${notMigrated.join(', ')}.`);
|
|
522
|
+
err.code = 'NON_MIGRATABLE_FIELDS';
|
|
523
|
+
err.notMigrated = notMigrated;
|
|
524
|
+
throw err;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const migrated = {
|
|
528
|
+
ConfigSchemaVersion: 2,
|
|
529
|
+
GatewayMode: legacy.GatewayMode || 'stdio-isolated',
|
|
530
|
+
GeneXus: { ...(legacy.GeneXus && typeof legacy.GeneXus === 'object' ? legacy.GeneXus : {}) },
|
|
531
|
+
Server: { ...(legacy.Server && typeof legacy.Server === 'object' ? legacy.Server : {}) },
|
|
532
|
+
Environment: {
|
|
533
|
+
ResolutionPolicy: env.ResolutionPolicy || 'strict'
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
const backupPath = (() => {
|
|
537
|
+
let candidate = `${source}.pre-migrate.bak`;
|
|
538
|
+
let suffix = 1;
|
|
539
|
+
while (fileSystem.existsSync(candidate)) candidate = `${source}.pre-migrate-${suffix++}.bak`;
|
|
540
|
+
return candidate;
|
|
541
|
+
})();
|
|
542
|
+
const targetExisted = fileSystem.existsSync(target);
|
|
543
|
+
const targetBackup = targetExisted ? `${target}.rollback-${process.pid}-${Date.now()}.bak` : null;
|
|
544
|
+
let wroteTarget = false;
|
|
545
|
+
try {
|
|
546
|
+
copyFileAtomic(source, backupPath, fileSystem);
|
|
547
|
+
if (targetExisted) copyFileAtomic(target, targetBackup, fileSystem);
|
|
548
|
+
fileSystem.mkdirSync(path.dirname(target), { recursive: true });
|
|
549
|
+
writeFileAtomic(target, JSON.stringify(migrated, null, 2), fileSystem);
|
|
550
|
+
wroteTarget = true;
|
|
551
|
+
const readBack = process.env.GENEXUS_MCP_MIGRATE_FAIL_READBACK
|
|
552
|
+
? null
|
|
553
|
+
: readJsonFileSafe(target, fileSystem);
|
|
554
|
+
if (!readBack || readBack.ConfigSchemaVersion !== 2 || JSON.stringify(readBack) !== JSON.stringify(migrated)) {
|
|
555
|
+
throw new Error('Migration read-back verification failed.');
|
|
556
|
+
}
|
|
557
|
+
if (targetBackup) {
|
|
558
|
+
try { fileSystem.rmSync(targetBackup, { force: true }); } catch { /* best effort */ }
|
|
559
|
+
}
|
|
560
|
+
return { sourcePath: source, targetPath: target, backupPath, readBack: true, migrated: Object.keys(migrated), notMigrated, rolledBack: false };
|
|
561
|
+
} catch (err) {
|
|
562
|
+
let rolledBack = false;
|
|
563
|
+
try {
|
|
564
|
+
if (targetExisted && targetBackup) copyFileAtomic(targetBackup, target, fileSystem);
|
|
565
|
+
else if (wroteTarget) fileSystem.rmSync(target, { force: true });
|
|
566
|
+
rolledBack = true;
|
|
567
|
+
} catch { /* report rollback failure */ }
|
|
568
|
+
if (targetBackup) {
|
|
569
|
+
try { fileSystem.rmSync(targetBackup, { force: true }); } catch { /* best effort */ }
|
|
570
|
+
}
|
|
571
|
+
err.rollback = { rolledBack };
|
|
572
|
+
err.backupPath = backupPath;
|
|
573
|
+
err.notMigrated = notMigrated;
|
|
574
|
+
throw err;
|
|
575
|
+
}
|
|
472
576
|
}
|
|
473
577
|
|
|
474
578
|
function resolveConfigPathNoMutate(cwd) {
|
|
@@ -869,6 +973,7 @@ function patchClientConfig(targetConfigPath, opts = {}) {
|
|
|
869
973
|
// user only finds out when each one fails with "Failed to connect".
|
|
870
974
|
const serverName = opts.serverName || DEFAULT_MCP_SERVER_NAME;
|
|
871
975
|
const force = Boolean(opts.force);
|
|
976
|
+
const fileSystem = opts.fs || fs;
|
|
872
977
|
const onlyExisting = opts.onlyExisting !== false;
|
|
873
978
|
const candidates = filterClientTargets(getClientConfigTargets(), {
|
|
874
979
|
ids: opts.ids,
|
|
@@ -923,11 +1028,16 @@ function patchClientConfig(targetConfigPath, opts = {}) {
|
|
|
923
1028
|
continue;
|
|
924
1029
|
}
|
|
925
1030
|
try {
|
|
926
|
-
|
|
927
|
-
applyClientEntry(client, getLauncher(client), targetConfigPath, {
|
|
1031
|
+
fileSystem.mkdirSync(path.dirname(client.path), { recursive: true });
|
|
1032
|
+
applyClientEntry(client, getLauncher(client), targetConfigPath, {
|
|
1033
|
+
serverName,
|
|
1034
|
+
force,
|
|
1035
|
+
globalConfig: Boolean(opts.globalConfig),
|
|
1036
|
+
fs: fileSystem
|
|
1037
|
+
});
|
|
928
1038
|
// Read-back: confirm the entry is actually present and the file still
|
|
929
1039
|
// parses, so a silently-corrupted write is reported as a failure.
|
|
930
|
-
if (!readClientCommandEntry(client, serverName)) {
|
|
1040
|
+
if (!readClientCommandEntry(client, serverName, { fs: fileSystem })) {
|
|
931
1041
|
throw new Error(`post-write verification failed (${serverName} entry not found after write)`);
|
|
932
1042
|
}
|
|
933
1043
|
patched.push(client.name);
|
|
@@ -978,8 +1088,8 @@ function removeClientEntry(client, opts = {}) {
|
|
|
978
1088
|
return getClientAdapter(client.format).remove(client, opts);
|
|
979
1089
|
}
|
|
980
1090
|
|
|
981
|
-
function applyMcpServersJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false } = {}) {
|
|
982
|
-
const parsed =
|
|
1091
|
+
function applyMcpServersJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false, globalConfig = false, fs: fileSystem = fs } = {}) {
|
|
1092
|
+
const parsed = fileSystem.existsSync(filePath) ? readJsonFileSafe(filePath, fileSystem) : {};
|
|
983
1093
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
984
1094
|
const cfgObj = parsed || {};
|
|
985
1095
|
cfgObj.mcpServers = cfgObj.mcpServers || {};
|
|
@@ -989,7 +1099,11 @@ function applyMcpServersJson(filePath, launcher, targetConfigPath, { serverName
|
|
|
989
1099
|
err.code = 'MCP_SERVER_COLLISION';
|
|
990
1100
|
throw err;
|
|
991
1101
|
}
|
|
992
|
-
|
|
1102
|
+
const serverEntry = { ...launcher };
|
|
1103
|
+
if (globalConfig && targetConfigPath) {
|
|
1104
|
+
serverEntry.env = { ...(serverEntry.env || {}), GX_CONFIG_PATH: targetConfigPath };
|
|
1105
|
+
}
|
|
1106
|
+
cfgObj.mcpServers[serverName] = serverEntry;
|
|
993
1107
|
// If registering default genexus18mcp, clean up legacy genexus/genexus18 entries only if they are not foreign HTTP servers
|
|
994
1108
|
if (serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
995
1109
|
if (cfgObj.mcpServers.genexus && !isThirdPartyMcpEntry(cfgObj.mcpServers.genexus)) {
|
|
@@ -1003,10 +1117,10 @@ function applyMcpServersJson(filePath, launcher, targetConfigPath, { serverName
|
|
|
1003
1117
|
delete cfgObj.mcpServers.genexus18;
|
|
1004
1118
|
}
|
|
1005
1119
|
}
|
|
1006
|
-
writeClientJson(filePath, cfgObj);
|
|
1120
|
+
writeClientJson(filePath, cfgObj, fileSystem);
|
|
1007
1121
|
}
|
|
1008
1122
|
|
|
1009
|
-
function removeMcpServersJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME } = {}) {
|
|
1123
|
+
function removeMcpServersJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME, fs: fileSystem = fs } = {}) {
|
|
1010
1124
|
const parsed = readJsonFileSafe(filePath);
|
|
1011
1125
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
1012
1126
|
const cfgObj = parsed || {};
|
|
@@ -1026,14 +1140,14 @@ function removeMcpServersJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME }
|
|
|
1026
1140
|
}
|
|
1027
1141
|
}
|
|
1028
1142
|
if (!removedAny) return false;
|
|
1029
|
-
writeClientJson(filePath, cfgObj);
|
|
1143
|
+
writeClientJson(filePath, cfgObj, fileSystem);
|
|
1030
1144
|
return true;
|
|
1031
1145
|
}
|
|
1032
1146
|
|
|
1033
1147
|
// VS Code native MCP lives in User\mcp.json and uses a top-level `servers` map
|
|
1034
1148
|
// with `type: "stdio"` (distinct from the `mcpServers` shape Claude/Cursor use).
|
|
1035
|
-
function applyVsCodeServersJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false } = {}) {
|
|
1036
|
-
const parsed =
|
|
1149
|
+
function applyVsCodeServersJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false, globalConfig = false, fs: fileSystem = fs } = {}) {
|
|
1150
|
+
const parsed = fileSystem.existsSync(filePath) ? readJsonFileSafe(filePath, fileSystem) : {};
|
|
1037
1151
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
1038
1152
|
const cfgObj = parsed || {};
|
|
1039
1153
|
cfgObj.servers = cfgObj.servers || {};
|
|
@@ -1043,11 +1157,14 @@ function applyVsCodeServersJson(filePath, launcher, targetConfigPath, { serverNa
|
|
|
1043
1157
|
err.code = 'MCP_SERVER_COLLISION';
|
|
1044
1158
|
throw err;
|
|
1045
1159
|
}
|
|
1046
|
-
|
|
1160
|
+
const serverEntry = {
|
|
1047
1161
|
type: 'stdio',
|
|
1048
|
-
...launcher
|
|
1049
|
-
env: { GX_CONFIG_PATH: targetConfigPath }
|
|
1162
|
+
...launcher
|
|
1050
1163
|
};
|
|
1164
|
+
if (globalConfig && targetConfigPath) {
|
|
1165
|
+
serverEntry.env = { ...(serverEntry.env || {}), GX_CONFIG_PATH: targetConfigPath };
|
|
1166
|
+
}
|
|
1167
|
+
cfgObj.servers[serverName] = serverEntry;
|
|
1051
1168
|
if (serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
1052
1169
|
if (cfgObj.servers.genexus && !isThirdPartyMcpEntry(cfgObj.servers.genexus)) {
|
|
1053
1170
|
delete cfgObj.servers.genexus;
|
|
@@ -1058,10 +1175,10 @@ function applyVsCodeServersJson(filePath, launcher, targetConfigPath, { serverNa
|
|
|
1058
1175
|
} else if (serverName === 'genexus' && cfgObj.servers.genexus18 && !isThirdPartyMcpEntry(cfgObj.servers.genexus18)) {
|
|
1059
1176
|
delete cfgObj.servers.genexus18;
|
|
1060
1177
|
}
|
|
1061
|
-
writeClientJson(filePath, cfgObj);
|
|
1178
|
+
writeClientJson(filePath, cfgObj, fileSystem);
|
|
1062
1179
|
}
|
|
1063
1180
|
|
|
1064
|
-
function removeVsCodeServersJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME } = {}) {
|
|
1181
|
+
function removeVsCodeServersJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME, fs: fileSystem = fs } = {}) {
|
|
1065
1182
|
const parsed = readJsonFileSafe(filePath);
|
|
1066
1183
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
1067
1184
|
const cfgObj = parsed || {};
|
|
@@ -1081,7 +1198,7 @@ function removeVsCodeServersJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAM
|
|
|
1081
1198
|
}
|
|
1082
1199
|
}
|
|
1083
1200
|
if (!removedAny) return false;
|
|
1084
|
-
writeClientJson(filePath, cfgObj);
|
|
1201
|
+
writeClientJson(filePath, cfgObj, fileSystem);
|
|
1085
1202
|
return true;
|
|
1086
1203
|
}
|
|
1087
1204
|
|
|
@@ -1102,8 +1219,8 @@ function getOpenCodeMcpContainer(cfgObj) {
|
|
|
1102
1219
|
};
|
|
1103
1220
|
}
|
|
1104
1221
|
|
|
1105
|
-
function applyOpenCodeJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false } = {}) {
|
|
1106
|
-
const parsed =
|
|
1222
|
+
function applyOpenCodeJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false, globalConfig = false, fs: fileSystem = fs } = {}) {
|
|
1223
|
+
const parsed = fileSystem.existsSync(filePath) ? readJsonFileSafe(filePath, fileSystem) : {};
|
|
1107
1224
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
1108
1225
|
const cfgObj = parsed || {};
|
|
1109
1226
|
// OpenCode configs carry a top-level $schema for editor validation; set it when
|
|
@@ -1116,12 +1233,15 @@ function applyOpenCodeJson(filePath, launcher, targetConfigPath, { serverName =
|
|
|
1116
1233
|
err.code = 'MCP_SERVER_COLLISION';
|
|
1117
1234
|
throw err;
|
|
1118
1235
|
}
|
|
1119
|
-
|
|
1236
|
+
const serverEntry = {
|
|
1120
1237
|
type: 'local',
|
|
1121
1238
|
command: [launcher.command, ...(launcher.args || [])],
|
|
1122
|
-
environment: { GX_CONFIG_PATH: targetConfigPath },
|
|
1123
1239
|
...(nested ? { disabled: false } : { enabled: true })
|
|
1124
1240
|
};
|
|
1241
|
+
if (globalConfig && targetConfigPath) {
|
|
1242
|
+
serverEntry.environment = { GX_CONFIG_PATH: targetConfigPath };
|
|
1243
|
+
}
|
|
1244
|
+
servers[serverName] = serverEntry;
|
|
1125
1245
|
if (serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
1126
1246
|
if (servers.genexus && !isThirdPartyMcpEntry(servers.genexus)) delete servers.genexus;
|
|
1127
1247
|
if (servers.genexus18 && !isThirdPartyMcpEntry(servers.genexus18)) delete servers.genexus18;
|
|
@@ -1142,10 +1262,10 @@ function applyOpenCodeJson(filePath, launcher, targetConfigPath, { serverName =
|
|
|
1142
1262
|
if (mcp[serverName] && !isThirdPartyMcpEntry(mcp[serverName])) delete mcp[serverName];
|
|
1143
1263
|
}
|
|
1144
1264
|
}
|
|
1145
|
-
writeClientJson(filePath, cfgObj);
|
|
1265
|
+
writeClientJson(filePath, cfgObj, fileSystem);
|
|
1146
1266
|
}
|
|
1147
1267
|
|
|
1148
|
-
function removeOpenCodeJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME } = {}) {
|
|
1268
|
+
function removeOpenCodeJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME, fs: fileSystem = fs } = {}) {
|
|
1149
1269
|
const parsed = readJsonFileSafe(filePath);
|
|
1150
1270
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
1151
1271
|
const cfgObj = parsed || {};
|
|
@@ -1177,7 +1297,7 @@ function removeOpenCodeJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME } =
|
|
|
1177
1297
|
}
|
|
1178
1298
|
}
|
|
1179
1299
|
if (!removedAny) return false;
|
|
1180
|
-
writeClientJson(filePath, cfgObj);
|
|
1300
|
+
writeClientJson(filePath, cfgObj, fileSystem);
|
|
1181
1301
|
return true;
|
|
1182
1302
|
}
|
|
1183
1303
|
|
|
@@ -1231,7 +1351,7 @@ function stripCodexServerBlocks(content, serverName) {
|
|
|
1231
1351
|
// [mcp_servers.genexus*] blocks and append fresh ones. Brittle on hand-edited
|
|
1232
1352
|
// files that put other keys after our blocks without a blank line, but
|
|
1233
1353
|
// adequate for the typical machine-managed config.
|
|
1234
|
-
function applyCodexToml(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false } = {}) {
|
|
1354
|
+
function applyCodexToml(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false, globalConfig = false } = {}) {
|
|
1235
1355
|
let existing = '';
|
|
1236
1356
|
if (fs.existsSync(filePath)) existing = fs.readFileSync(filePath, 'utf8');
|
|
1237
1357
|
const existingEntry = extractCodexTomlEntry(existing, serverName);
|
|
@@ -1263,9 +1383,11 @@ function applyCodexToml(filePath, launcher, targetConfigPath, { serverName = DEF
|
|
|
1263
1383
|
lines.push(`[mcp_servers.${serverName}]`);
|
|
1264
1384
|
lines.push(`command = ${tomlString(launcher.command)}`);
|
|
1265
1385
|
lines.push(`args = [${args.map(tomlString).join(', ')}]`);
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1386
|
+
if (globalConfig && targetConfigPath) {
|
|
1387
|
+
lines.push('');
|
|
1388
|
+
lines.push(`[mcp_servers.${serverName}.env]`);
|
|
1389
|
+
lines.push(`GX_CONFIG_PATH = ${tomlString(targetConfigPath)}`);
|
|
1390
|
+
}
|
|
1269
1391
|
lines.push('');
|
|
1270
1392
|
writeClientText(filePath, stripped + lines.join('\n'));
|
|
1271
1393
|
}
|
|
@@ -1321,12 +1443,12 @@ function normalizeExePath(p) {
|
|
|
1321
1443
|
return s;
|
|
1322
1444
|
}
|
|
1323
1445
|
|
|
1324
|
-
function readClientCommandEntry(client, serverName = DEFAULT_MCP_SERVER_NAME) {
|
|
1446
|
+
function readClientCommandEntry(client, serverName = DEFAULT_MCP_SERVER_NAME, { fs: fileSystem = fs } = {}) {
|
|
1325
1447
|
if (client.writeSupported === false) return null;
|
|
1326
|
-
if (!
|
|
1448
|
+
if (!fileSystem.existsSync(client.path)) return null;
|
|
1327
1449
|
try {
|
|
1328
1450
|
if (client.format === 'mcpServers') {
|
|
1329
|
-
const parsed = readJsonFileSafe(client.path);
|
|
1451
|
+
const parsed = readJsonFileSafe(client.path, fileSystem);
|
|
1330
1452
|
if (!parsed || typeof parsed !== 'object') return null;
|
|
1331
1453
|
let entry = parsed.mcpServers && parsed.mcpServers[serverName];
|
|
1332
1454
|
if (!entry && serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
@@ -1345,7 +1467,7 @@ function readClientCommandEntry(client, serverName = DEFAULT_MCP_SERVER_NAME) {
|
|
|
1345
1467
|
};
|
|
1346
1468
|
}
|
|
1347
1469
|
if (client.format === 'opencode') {
|
|
1348
|
-
const parsed = readJsonFileSafe(client.path);
|
|
1470
|
+
const parsed = readJsonFileSafe(client.path, fileSystem);
|
|
1349
1471
|
if (!parsed || typeof parsed !== 'object') return null;
|
|
1350
1472
|
let entry = parsed.mcp?.servers?.[serverName] || parsed.mcp?.[serverName];
|
|
1351
1473
|
if (!entry && serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
@@ -1374,7 +1496,7 @@ function readClientCommandEntry(client, serverName = DEFAULT_MCP_SERVER_NAME) {
|
|
|
1374
1496
|
};
|
|
1375
1497
|
}
|
|
1376
1498
|
if (client.format === 'vscode-servers') {
|
|
1377
|
-
const parsed = readJsonFileSafe(client.path);
|
|
1499
|
+
const parsed = readJsonFileSafe(client.path, fileSystem);
|
|
1378
1500
|
if (!parsed || typeof parsed !== 'object') return null;
|
|
1379
1501
|
let entry = parsed.servers && parsed.servers[serverName];
|
|
1380
1502
|
if (!entry && serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
@@ -1729,13 +1851,31 @@ function applyLauncherConfigOrExit({ cwd, stderr, quiet }) {
|
|
|
1729
1851
|
return { ok: true };
|
|
1730
1852
|
}
|
|
1731
1853
|
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
log(
|
|
1854
|
+
const discoveredGxPath = discoverGeneXusInstallation();
|
|
1855
|
+
if (!discoveredGxPath) {
|
|
1856
|
+
log('[genexus-mcp] ERROR: No config.json found and GeneXus installation auto-discovery failed.');
|
|
1735
1857
|
log('[genexus-mcp] Fix with: npx genexus-mcp init --interactive');
|
|
1736
1858
|
return { ok: false };
|
|
1737
1859
|
}
|
|
1738
1860
|
|
|
1861
|
+
const userMcpDir = path.join(os.homedir(), '.genexus-mcp');
|
|
1862
|
+
const userMcpConfigPath = path.join(userMcpDir, 'config.json');
|
|
1863
|
+
|
|
1864
|
+
|
|
1865
|
+
if (!directoryLooksLikeKnowledgeBase(cwd)) {
|
|
1866
|
+
if (fs.existsSync(userMcpConfigPath)) {
|
|
1867
|
+
process.env.GX_CONFIG_PATH = userMcpConfigPath;
|
|
1868
|
+
return { ok: true };
|
|
1869
|
+
}
|
|
1870
|
+
log(`[genexus-mcp] Auto-discovered GeneXus at: ${discoveredGxPath}`);
|
|
1871
|
+
log(`[genexus-mcp] Current directory is not a GeneXus KB. Generating neutral user config at: ${userMcpConfigPath}`);
|
|
1872
|
+
fs.mkdirSync(userMcpDir, { recursive: true });
|
|
1873
|
+
const neutralConfig = generateNeutralConfig(discoveredGxPath);
|
|
1874
|
+
writeFileAtomic(userMcpConfigPath, JSON.stringify(neutralConfig, null, 2));
|
|
1875
|
+
process.env.GX_CONFIG_PATH = userMcpConfigPath;
|
|
1876
|
+
return { ok: true };
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1739
1879
|
const kbIdentity = readGeneXusKbIdentity(cwd);
|
|
1740
1880
|
if (!kbIdentity.major) {
|
|
1741
1881
|
log('[genexus-mcp] ERROR: Zero-config could not determine the KB GeneXus major safely.');
|
|
@@ -1776,6 +1916,7 @@ function applyLauncherConfigOrExit({ cwd, stderr, quiet }) {
|
|
|
1776
1916
|
|
|
1777
1917
|
module.exports = {
|
|
1778
1918
|
generateConfig,
|
|
1919
|
+
generateNeutralConfig,
|
|
1779
1920
|
getGatewayExePath,
|
|
1780
1921
|
getToolDefinitionsPath,
|
|
1781
1922
|
getGeneXusVersionCatalog,
|
|
@@ -1787,6 +1928,7 @@ module.exports = {
|
|
|
1787
1928
|
directoryLooksLikeKnowledgeBase,
|
|
1788
1929
|
readJsonFileSafe,
|
|
1789
1930
|
resolveConfigPathNoMutate,
|
|
1931
|
+
migrateLegacyConfig,
|
|
1790
1932
|
createConfigFile,
|
|
1791
1933
|
patchClientConfig,
|
|
1792
1934
|
unpatchClientConfig,
|