genexus-mcp 2.48.0 → 2.49.1
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/cli/commands/axi.js +44 -15
- package/cli/index.js +18 -0
- package/cli/lib/config.js +333 -86
- package/cli/run.test.js +163 -16
- package/package.json +1 -1
- package/publish/GxMcp.Gateway.deps.json +2 -2
- package/publish/GxMcp.Gateway.dll +0 -0
- package/publish/GxMcp.Gateway.exe +0 -0
- package/publish/config.json +8 -8
- package/publish/worker/GxMcp.Worker.exe +0 -0
package/cli/commands/axi.js
CHANGED
|
@@ -27,7 +27,8 @@ const {
|
|
|
27
27
|
switchActiveKb,
|
|
28
28
|
isPathLikelyAppLockerBlocked,
|
|
29
29
|
normalizeExePath,
|
|
30
|
-
readClientCommandEntry
|
|
30
|
+
readClientCommandEntry,
|
|
31
|
+
DEFAULT_MCP_SERVER_NAME
|
|
31
32
|
} = require('../lib/config');
|
|
32
33
|
|
|
33
34
|
function resolveClientIds(options) {
|
|
@@ -1154,6 +1155,9 @@ async function handleLayout(subcommand, options, ctx) {
|
|
|
1154
1155
|
|
|
1155
1156
|
function buildInteractiveInitHelp(patchResult) {
|
|
1156
1157
|
const help = [];
|
|
1158
|
+
if (patchResult.failed && patchResult.failed.some((f) => f.reason && f.reason.includes('third-party or HTTP MCP server'))) {
|
|
1159
|
+
help.push('A collision with an existing MCP server (e.g. official GeneXus MCP) was detected. Re-run init with --server-name=<customName> (e.g. --server-name Gx18byLennix) or pass --force.');
|
|
1160
|
+
}
|
|
1157
1161
|
if (patchResult.patched.length === 0) {
|
|
1158
1162
|
help.push('Set `GX_CONFIG_PATH` in your MCP client env to the generated config path.');
|
|
1159
1163
|
} else if (process.platform === 'win32' && !process.env.GENEXUS_MCP_GATEWAY_EXE) {
|
|
@@ -1228,8 +1232,10 @@ async function runInteractiveInit(ctx) {
|
|
|
1228
1232
|
}
|
|
1229
1233
|
|
|
1230
1234
|
const created = createConfigFile(finalKb, finalGx);
|
|
1235
|
+
const serverName = ctx.options && ctx.options.serverName ? ctx.options.serverName : DEFAULT_MCP_SERVER_NAME;
|
|
1236
|
+
const force = Boolean(ctx.options && ctx.options.force);
|
|
1231
1237
|
const patchResult = selectedIds.length
|
|
1232
|
-
? patchClientConfig(created.targetConfigPath, { ids: selectedIds, onlyExisting: false })
|
|
1238
|
+
? patchClientConfig(created.targetConfigPath, { ids: selectedIds, onlyExisting: false, serverName, force })
|
|
1233
1239
|
: { patched: [], failed: [], skipped: [] };
|
|
1234
1240
|
|
|
1235
1241
|
return {
|
|
@@ -1405,7 +1411,9 @@ async function handleInit(options, ctx) {
|
|
|
1405
1411
|
}
|
|
1406
1412
|
patchResult = patchClientConfig(created.targetConfigPath, {
|
|
1407
1413
|
ids,
|
|
1408
|
-
onlyExisting: !options.allClients
|
|
1414
|
+
onlyExisting: !options.allClients,
|
|
1415
|
+
serverName: options.serverName || DEFAULT_MCP_SERVER_NAME,
|
|
1416
|
+
force: options.force
|
|
1409
1417
|
});
|
|
1410
1418
|
}
|
|
1411
1419
|
|
|
@@ -1422,6 +1430,10 @@ async function handleInit(options, ctx) {
|
|
|
1422
1430
|
}
|
|
1423
1431
|
|
|
1424
1432
|
const help = [];
|
|
1433
|
+
if (patchResult.failed.some((f) => f.reason && f.reason.includes('third-party or HTTP MCP server'))) {
|
|
1434
|
+
help.push('A collision with an existing MCP server (e.g. official GeneXus MCP) was detected on one or more clients.');
|
|
1435
|
+
help.push('Re-run init with --server-name=<customName> (e.g. --server-name Gx18byLennix), pass --force, or pass --no-write-clients.');
|
|
1436
|
+
}
|
|
1425
1437
|
if (patchResult.patched.length === 0 && shouldWriteClients) {
|
|
1426
1438
|
help.push('No installed AI client config was found to patch. Pass --all-clients to write configs for all known clients, or register manually.');
|
|
1427
1439
|
help.push(`For a GLOBAL (all-projects) registration that does not depend on the current directory, point your client at this config via the GX_CONFIG_PATH env var, e.g.: claude mcp add genexus -e GX_CONFIG_PATH="${created.targetConfigPath}" -- <launcher>`);
|
|
@@ -1752,19 +1764,20 @@ async function handleUninstall(options, ctx) {
|
|
|
1752
1764
|
|
|
1753
1765
|
async function handleClients(subcommand, options, ctx) {
|
|
1754
1766
|
const sub = subcommand || 'list';
|
|
1767
|
+
const serverName = options.serverName || DEFAULT_MCP_SERVER_NAME;
|
|
1755
1768
|
|
|
1756
1769
|
if (sub === 'list') {
|
|
1757
|
-
const rows = clientsStatus();
|
|
1770
|
+
const rows = clientsStatus({ serverName });
|
|
1758
1771
|
const installedCount = rows.filter((r) => r.installed).length;
|
|
1759
1772
|
const registeredCount = rows.filter((r) => r.registered).length;
|
|
1760
1773
|
const help = [];
|
|
1761
1774
|
const installedUnregistered = rows.filter((r) => r.installed && !r.registered && r.writeSupported);
|
|
1762
1775
|
if (installedUnregistered.length > 0) {
|
|
1763
|
-
help.push(`Register installed-but-unregistered agents: genexus-mcp clients add --clients ${installedUnregistered.map((r) => r.id).join(',')}`);
|
|
1776
|
+
help.push(`Register installed-but-unregistered agents: genexus-mcp clients add --clients ${installedUnregistered.map((r) => r.id).join(',')}${serverName !== DEFAULT_MCP_SERVER_NAME ? ` --server-name ${serverName}` : ''}`);
|
|
1764
1777
|
}
|
|
1765
1778
|
const stale = rows.filter((r) => r.commandStale);
|
|
1766
1779
|
if (stale.length > 0) {
|
|
1767
|
-
help.push(`These clients point at a missing gateway exe (will fail to connect) — re-register: genexus-mcp clients add --clients ${stale.map((r) => r.id).join(',')}`);
|
|
1780
|
+
help.push(`These clients point at a missing gateway exe (will fail to connect) — re-register: genexus-mcp clients add --clients ${stale.map((r) => r.id).join(',')}${serverName !== DEFAULT_MCP_SERVER_NAME ? ` --server-name ${serverName}` : ''}`);
|
|
1768
1781
|
}
|
|
1769
1782
|
for (const r of rows) {
|
|
1770
1783
|
if (r.installed && !r.writeSupported && r.note) help.push(`${r.name}: ${r.note}`);
|
|
@@ -1774,7 +1787,7 @@ async function handleClients(subcommand, options, ctx) {
|
|
|
1774
1787
|
envelope: {
|
|
1775
1788
|
ok: {
|
|
1776
1789
|
clients: rows,
|
|
1777
|
-
summary: { total: rows.length, installed: installedCount, registered: registeredCount }
|
|
1790
|
+
summary: { total: rows.length, installed: installedCount, registered: registeredCount, serverName }
|
|
1778
1791
|
},
|
|
1779
1792
|
help
|
|
1780
1793
|
}
|
|
@@ -1808,7 +1821,12 @@ async function handleClients(subcommand, options, ctx) {
|
|
|
1808
1821
|
let patch;
|
|
1809
1822
|
try {
|
|
1810
1823
|
// Explicit add: write even if install markers are absent (the user asked for it).
|
|
1811
|
-
patch = patchClientConfig(configPath, {
|
|
1824
|
+
patch = patchClientConfig(configPath, {
|
|
1825
|
+
ids,
|
|
1826
|
+
onlyExisting: false,
|
|
1827
|
+
serverName,
|
|
1828
|
+
force: options.force
|
|
1829
|
+
});
|
|
1812
1830
|
} catch (err) {
|
|
1813
1831
|
return {
|
|
1814
1832
|
exitCode: ctx.EXIT_CODES.ERROR,
|
|
@@ -1820,11 +1838,16 @@ async function handleClients(subcommand, options, ctx) {
|
|
|
1820
1838
|
}
|
|
1821
1839
|
const help = [];
|
|
1822
1840
|
if (patch.patched.length > 0) help.push('Restart the affected AI client(s) to load the new MCP config.');
|
|
1823
|
-
if (patch.failed.
|
|
1841
|
+
if (patch.failed.some((f) => f.reason && f.reason.includes('third-party or HTTP MCP server'))) {
|
|
1842
|
+
help.push('A collision with an existing MCP server (e.g. official GeneXus MCP) was detected.');
|
|
1843
|
+
help.push('Use --server-name=<customName> (e.g. --server-name Gx18byLennix) or pass --force.');
|
|
1844
|
+
} else if (patch.failed.length > 0) {
|
|
1845
|
+
help.push('Some clients failed (see meta.failedClients).');
|
|
1846
|
+
}
|
|
1824
1847
|
return {
|
|
1825
1848
|
exitCode: ctx.EXIT_CODES.OK,
|
|
1826
1849
|
envelope: {
|
|
1827
|
-
ok: { action: 'clients.add', configPath, patchedClients: patch.patched, patchedCount: patch.patched.length },
|
|
1850
|
+
ok: { action: 'clients.add', configPath, serverName, patchedClients: patch.patched, patchedCount: patch.patched.length },
|
|
1828
1851
|
help,
|
|
1829
1852
|
meta: { failedClients: patch.failed, skippedClients: patch.skipped }
|
|
1830
1853
|
}
|
|
@@ -1832,13 +1855,16 @@ async function handleClients(subcommand, options, ctx) {
|
|
|
1832
1855
|
}
|
|
1833
1856
|
|
|
1834
1857
|
// remove
|
|
1835
|
-
const unpatch = unpatchClientConfig(
|
|
1858
|
+
const unpatch = unpatchClientConfig({
|
|
1859
|
+
...(ids ? { ids } : {}),
|
|
1860
|
+
serverName
|
|
1861
|
+
});
|
|
1836
1862
|
const help = [];
|
|
1837
1863
|
if (unpatch.removed.length > 0) help.push('Restart the affected AI client(s) to drop the stale MCP connection.');
|
|
1838
1864
|
return {
|
|
1839
1865
|
exitCode: ctx.EXIT_CODES.OK,
|
|
1840
1866
|
envelope: {
|
|
1841
|
-
ok: { action: 'clients.remove', removedClients: unpatch.removed, removedCount: unpatch.removed.length },
|
|
1867
|
+
ok: { action: 'clients.remove', serverName, removedClients: unpatch.removed, removedCount: unpatch.removed.length },
|
|
1842
1868
|
help,
|
|
1843
1869
|
meta: { skippedClients: unpatch.skipped, failedClients: unpatch.failed }
|
|
1844
1870
|
}
|
|
@@ -2009,10 +2035,11 @@ function commandHelpMap() {
|
|
|
2009
2035
|
examples: ['genexus-mcp config show', 'genexus-mcp config show --full --format json']
|
|
2010
2036
|
},
|
|
2011
2037
|
init: {
|
|
2012
|
-
usage: 'genexus-mcp init [--kb <path>] [--gx <path>] [--no-write-clients] [--clients <csv>] [--all-clients] [--no-smoke] [--warm] [--format ...] OR genexus-mcp init --interactive',
|
|
2038
|
+
usage: 'genexus-mcp init [--kb <path>] [--gx <path>] [--server-name <name>] [--force] [--no-write-clients] [--clients <csv>] [--all-clients] [--no-smoke] [--warm] [--format ...] OR genexus-mcp init --interactive',
|
|
2013
2039
|
examples: [
|
|
2014
2040
|
'genexus-mcp init # zero-config: auto-discovers GX + KB, and registers detected AI clients',
|
|
2015
2041
|
'genexus-mcp init --kb "C:\\KBs\\MyKB" --gx "C:\\Program Files (x86)\\GeneXus\\GeneXus18"',
|
|
2042
|
+
'genexus-mcp init --server-name Gx18byLennix # register with custom MCP server key (coexists with official GeneXus MCP)',
|
|
2016
2043
|
'genexus-mcp init --interactive # prompts per detected agent (Claude Desktop/Code, Gemini CLI, Cursor, Codex CLI, OpenCode, ...)',
|
|
2017
2044
|
'genexus-mcp init --kb <path> --gx <path> --clients claude-code,gemini-cli,cursor # register only these',
|
|
2018
2045
|
'genexus-mcp init --kb <path> --gx <path> --no-write-clients # write config.json only; register clients yourself',
|
|
@@ -2038,12 +2065,14 @@ function commandHelpMap() {
|
|
|
2038
2065
|
]
|
|
2039
2066
|
},
|
|
2040
2067
|
clients: {
|
|
2041
|
-
usage: 'genexus-mcp clients [list] [--format ...] OR genexus-mcp clients add --clients <csv> OR genexus-mcp clients remove [--clients <csv>]',
|
|
2068
|
+
usage: 'genexus-mcp clients [list] [--server-name <name>] [--format ...] OR genexus-mcp clients add --clients <csv> [--server-name <name>] [--force] OR genexus-mcp clients remove [--clients <csv>] [--server-name <name>]',
|
|
2042
2069
|
examples: [
|
|
2043
2070
|
'genexus-mcp clients # show every AI agent: installed? registered? where?',
|
|
2044
2071
|
'genexus-mcp clients --format json',
|
|
2045
2072
|
'genexus-mcp clients add --clients antigravity,vscode',
|
|
2046
|
-
'genexus-mcp clients
|
|
2073
|
+
'genexus-mcp clients add --clients cursor --server-name Gx18byLennix',
|
|
2074
|
+
'genexus-mcp clients remove --clients cursor',
|
|
2075
|
+
'genexus-mcp clients remove --clients cursor --server-name Gx18byLennix'
|
|
2047
2076
|
]
|
|
2048
2077
|
},
|
|
2049
2078
|
llm: {
|
package/cli/index.js
CHANGED
|
@@ -43,6 +43,8 @@ const GLOBAL_DEFAULTS = {
|
|
|
43
43
|
writeClients: false,
|
|
44
44
|
clients: null,
|
|
45
45
|
allClients: false,
|
|
46
|
+
serverName: null,
|
|
47
|
+
force: false,
|
|
46
48
|
mcpSmoke: false,
|
|
47
49
|
dump: false,
|
|
48
50
|
noSmoke: false,
|
|
@@ -207,6 +209,22 @@ function parseArgs(argv) {
|
|
|
207
209
|
case 'all-clients':
|
|
208
210
|
result.options.allClients = true;
|
|
209
211
|
break;
|
|
212
|
+
case 'server-name': {
|
|
213
|
+
const val = takeValue();
|
|
214
|
+
if (val) {
|
|
215
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(val)) {
|
|
216
|
+
result.unknownFlags.push(`--server-name must be alphanumeric (letters, digits, _, -), got: "${val}"`);
|
|
217
|
+
} else {
|
|
218
|
+
result.options.serverName = val;
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
result.unknownFlags.push('--server-name requires a value');
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case 'force':
|
|
226
|
+
result.options.force = true;
|
|
227
|
+
break;
|
|
210
228
|
case 'action': {
|
|
211
229
|
const val = takeValue();
|
|
212
230
|
if (val) result.options.action = val;
|
package/cli/lib/config.js
CHANGED
|
@@ -453,6 +453,57 @@ function getLauncher() {
|
|
|
453
453
|
: { command: process.platform === 'win32' ? 'npx.cmd' : 'npx', args: ['-y', 'genexus-mcp@latest'] };
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
+
const DEFAULT_MCP_SERVER_NAME = 'genexus18mcp';
|
|
457
|
+
|
|
458
|
+
function escapeRegex(str) {
|
|
459
|
+
return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Detect whether an MCP server entry belongs to a third-party or official MCP
|
|
463
|
+
// server (e.g. official GeneXus MCP over HTTP, or another tool).
|
|
464
|
+
function isThirdPartyMcpEntry(entry) {
|
|
465
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
466
|
+
// Explicit HTTP / SSE / remote URLs are third-party / official GeneXus MCP servers
|
|
467
|
+
if (entry.url || entry.type === 'http' || entry.type === 'sse' || entry.type === 'remote') {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
// Environment markers written by genexus-mcp
|
|
471
|
+
const env = entry.env || entry.environment;
|
|
472
|
+
if (env && typeof env === 'object') {
|
|
473
|
+
if (env.GX_CONFIG_PATH || env.GX_PATH || env.GENEXUS_MCP_GATEWAY_EXE) {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// Command string or args mentioning genexus-mcp / GxMcp.Gateway / start_mcp.bat
|
|
478
|
+
if (typeof entry.command === 'string') {
|
|
479
|
+
if (/(^|[\\/])(genexus-mcp(\.cmd)?|GxMcp\.Gateway(\.exe)?|start_mcp\.bat)$/i.test(entry.command)) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
if (/(^|[\\/])(npx|npx\.cmd)$/i.test(entry.command) && (!entry.args || entry.args.length === 0)) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (Array.isArray(entry.args) && entry.args.some((a) => typeof a === 'string' && /genexus-mcp/i.test(a))) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
if (Array.isArray(entry.command)) {
|
|
490
|
+
if (entry.command.some((a) => typeof a === 'string' && /(genexus-mcp|GxMcp\.Gateway|start_mcp\.bat)/i.test(a))) {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
if (entry.command.length === 1 && /(^|[\\/])(npx|npx\.cmd)$/i.test(entry.command[0])) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Any other custom command without our markers is third-party
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function isOurMcpEntry(entry, key) {
|
|
502
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
503
|
+
if (key === 'genexus18' || key === 'genexus18mcp') return true;
|
|
504
|
+
return !isThirdPartyMcpEntry(entry);
|
|
505
|
+
}
|
|
506
|
+
|
|
456
507
|
// Antigravity (Google's agentic IDE) ships its MCP config under ~/.gemini.
|
|
457
508
|
// The newer unified location (shared across Antigravity CLI / IDE / SDK) is
|
|
458
509
|
// ~/.gemini/config/mcp_config.json; the older IDE-specific one is
|
|
@@ -664,24 +715,29 @@ function clientCommandHealth(entry) {
|
|
|
664
715
|
// is genexus registered, where, what launcher command it points at, and whether
|
|
665
716
|
// that command is stale. Backs the `genexus-mcp clients` command.
|
|
666
717
|
function clientsStatus(opts = {}) {
|
|
718
|
+
const serverName = opts.serverName || DEFAULT_MCP_SERVER_NAME;
|
|
667
719
|
const targets = filterClientTargets(getClientConfigTargets(), {
|
|
668
720
|
ids: opts.ids,
|
|
669
721
|
platform: process.platform
|
|
670
722
|
});
|
|
671
723
|
return targets.map((client) => {
|
|
672
724
|
const det = detectClientInstalled(client);
|
|
673
|
-
const entry = readClientCommandEntry(client);
|
|
725
|
+
const entry = readClientCommandEntry(client, serverName);
|
|
674
726
|
const health = clientCommandHealth(entry);
|
|
727
|
+
const isThirdParty = entry && !isOurMcpEntry(entry.raw || entry, serverName);
|
|
675
728
|
return {
|
|
676
729
|
id: client.id,
|
|
677
730
|
name: client.name,
|
|
678
731
|
installed: det.installed,
|
|
679
732
|
registered: entry !== null,
|
|
733
|
+
serverName,
|
|
734
|
+
isThirdParty: Boolean(isThirdParty),
|
|
680
735
|
writeSupported: client.writeSupported !== false,
|
|
681
736
|
configPath: client.path,
|
|
682
737
|
command: entry && entry.command ? entry.command : null,
|
|
738
|
+
url: entry && entry.url ? entry.url : null,
|
|
683
739
|
commandStale: health.stale,
|
|
684
|
-
commandStaleReason: health.reason,
|
|
740
|
+
commandStaleReason: health.reason || (isThirdParty ? 'configured as third-party / HTTP MCP server (e.g. official GeneXus MCP)' : null),
|
|
685
741
|
detectedAt: det.markerHit || (det.hasConfig ? client.path : null),
|
|
686
742
|
note: client.writeSupported === false ? (client.manualNote || null) : null
|
|
687
743
|
};
|
|
@@ -713,6 +769,8 @@ function patchClientConfig(targetConfigPath, opts = {}) {
|
|
|
713
769
|
throw err;
|
|
714
770
|
}
|
|
715
771
|
|
|
772
|
+
const serverName = opts.serverName || DEFAULT_MCP_SERVER_NAME;
|
|
773
|
+
const force = Boolean(opts.force);
|
|
716
774
|
const launcher = getLauncher();
|
|
717
775
|
const onlyExisting = opts.onlyExisting !== false;
|
|
718
776
|
const candidates = filterClientTargets(getClientConfigTargets(), {
|
|
@@ -742,11 +800,11 @@ function patchClientConfig(targetConfigPath, opts = {}) {
|
|
|
742
800
|
}
|
|
743
801
|
try {
|
|
744
802
|
fs.mkdirSync(path.dirname(client.path), { recursive: true });
|
|
745
|
-
applyClientEntry(client, launcher, targetConfigPath);
|
|
803
|
+
applyClientEntry(client, launcher, targetConfigPath, { serverName, force });
|
|
746
804
|
// Read-back: confirm the entry is actually present and the file still
|
|
747
805
|
// parses, so a silently-corrupted write is reported as a failure.
|
|
748
|
-
if (!readClientCommandEntry(client)) {
|
|
749
|
-
throw new Error(
|
|
806
|
+
if (!readClientCommandEntry(client, serverName)) {
|
|
807
|
+
throw new Error(`post-write verification failed (${serverName} entry not found after write)`);
|
|
750
808
|
}
|
|
751
809
|
patched.push(client.name);
|
|
752
810
|
} catch (err) {
|
|
@@ -758,6 +816,7 @@ function patchClientConfig(targetConfigPath, opts = {}) {
|
|
|
758
816
|
}
|
|
759
817
|
|
|
760
818
|
function unpatchClientConfig(opts = {}) {
|
|
819
|
+
const serverName = opts.serverName || DEFAULT_MCP_SERVER_NAME;
|
|
761
820
|
const targets = filterClientTargets(getClientConfigTargets(), {
|
|
762
821
|
ids: opts.ids,
|
|
763
822
|
onlyExisting: true,
|
|
@@ -774,9 +833,9 @@ function unpatchClientConfig(opts = {}) {
|
|
|
774
833
|
continue;
|
|
775
834
|
}
|
|
776
835
|
try {
|
|
777
|
-
const wasRemoved = removeClientEntry(client);
|
|
836
|
+
const wasRemoved = removeClientEntry(client, { serverName });
|
|
778
837
|
if (wasRemoved) removed.push(client.name);
|
|
779
|
-
else skipped.push({ client: client.name, reason:
|
|
838
|
+
else skipped.push({ client: client.name, reason: `no ${serverName} entry` });
|
|
780
839
|
} catch (err) {
|
|
781
840
|
failed.push({ client: client.name, reason: err && err.message ? err.message : 'Unknown error' });
|
|
782
841
|
}
|
|
@@ -785,59 +844,79 @@ function unpatchClientConfig(opts = {}) {
|
|
|
785
844
|
return { removed, skipped, failed };
|
|
786
845
|
}
|
|
787
846
|
|
|
788
|
-
function applyClientEntry(client, launcher, targetConfigPath) {
|
|
847
|
+
function applyClientEntry(client, launcher, targetConfigPath, opts = {}) {
|
|
789
848
|
switch (client.format) {
|
|
790
849
|
case 'mcpServers':
|
|
791
|
-
return applyMcpServersJson(client.path, launcher, targetConfigPath);
|
|
850
|
+
return applyMcpServersJson(client.path, launcher, targetConfigPath, opts);
|
|
792
851
|
case 'opencode':
|
|
793
|
-
return applyOpenCodeJson(client.path, launcher, targetConfigPath);
|
|
852
|
+
return applyOpenCodeJson(client.path, launcher, targetConfigPath, opts);
|
|
794
853
|
case 'codex-toml':
|
|
795
|
-
return applyCodexToml(client.path, launcher, targetConfigPath);
|
|
854
|
+
return applyCodexToml(client.path, launcher, targetConfigPath, opts);
|
|
796
855
|
case 'vscode-servers':
|
|
797
|
-
return applyVsCodeServersJson(client.path, launcher, targetConfigPath);
|
|
856
|
+
return applyVsCodeServersJson(client.path, launcher, targetConfigPath, opts);
|
|
798
857
|
default:
|
|
799
858
|
throw new Error(`Unknown client format: ${client.format}`);
|
|
800
859
|
}
|
|
801
860
|
}
|
|
802
861
|
|
|
803
|
-
function removeClientEntry(client) {
|
|
862
|
+
function removeClientEntry(client, opts = {}) {
|
|
804
863
|
switch (client.format) {
|
|
805
864
|
case 'mcpServers':
|
|
806
|
-
return removeMcpServersJson(client.path);
|
|
865
|
+
return removeMcpServersJson(client.path, opts);
|
|
807
866
|
case 'opencode':
|
|
808
|
-
return removeOpenCodeJson(client.path);
|
|
867
|
+
return removeOpenCodeJson(client.path, opts);
|
|
809
868
|
case 'codex-toml':
|
|
810
|
-
return removeCodexToml(client.path);
|
|
869
|
+
return removeCodexToml(client.path, opts);
|
|
811
870
|
case 'vscode-servers':
|
|
812
|
-
return removeVsCodeServersJson(client.path);
|
|
871
|
+
return removeVsCodeServersJson(client.path, opts);
|
|
813
872
|
default:
|
|
814
873
|
throw new Error(`Unknown client format: ${client.format}`);
|
|
815
874
|
}
|
|
816
875
|
}
|
|
817
876
|
|
|
818
|
-
function applyMcpServersJson(filePath, launcher, targetConfigPath) {
|
|
877
|
+
function applyMcpServersJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false } = {}) {
|
|
819
878
|
const parsed = fs.existsSync(filePath) ? readJsonFileSafe(filePath) : {};
|
|
820
879
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
821
880
|
const cfgObj = parsed || {};
|
|
822
881
|
cfgObj.mcpServers = cfgObj.mcpServers || {};
|
|
823
|
-
cfgObj.mcpServers
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
882
|
+
const existing = cfgObj.mcpServers[serverName];
|
|
883
|
+
if (existing && !force && isThirdPartyMcpEntry(existing)) {
|
|
884
|
+
const err = new Error(`Existing '${serverName}' entry is a third-party or HTTP MCP server (e.g. official GeneXus MCP). Refusing to overwrite. Use --server-name=<customName> (e.g. --server-name Gx18byLennix) or --no-write-clients.`);
|
|
885
|
+
err.code = 'MCP_SERVER_COLLISION';
|
|
886
|
+
throw err;
|
|
887
|
+
}
|
|
888
|
+
cfgObj.mcpServers[serverName] = { ...launcher, env: { GX_CONFIG_PATH: targetConfigPath } };
|
|
889
|
+
// If registering default genexus18mcp, clean up legacy genexus/genexus18 entries only if they are not foreign HTTP servers
|
|
890
|
+
if (serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
891
|
+
if (cfgObj.mcpServers.genexus && !isThirdPartyMcpEntry(cfgObj.mcpServers.genexus)) {
|
|
892
|
+
delete cfgObj.mcpServers.genexus;
|
|
893
|
+
}
|
|
894
|
+
if (cfgObj.mcpServers.genexus18 && !isThirdPartyMcpEntry(cfgObj.mcpServers.genexus18)) {
|
|
895
|
+
delete cfgObj.mcpServers.genexus18;
|
|
896
|
+
}
|
|
897
|
+
} else if (serverName === 'genexus') {
|
|
898
|
+
if (cfgObj.mcpServers.genexus18 && !isThirdPartyMcpEntry(cfgObj.mcpServers.genexus18)) {
|
|
899
|
+
delete cfgObj.mcpServers.genexus18;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
827
902
|
writeClientJson(filePath, cfgObj);
|
|
828
903
|
}
|
|
829
904
|
|
|
830
|
-
function removeMcpServersJson(filePath) {
|
|
905
|
+
function removeMcpServersJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME } = {}) {
|
|
831
906
|
const parsed = readJsonFileSafe(filePath);
|
|
832
907
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
833
908
|
const cfgObj = parsed || {};
|
|
834
909
|
if (!cfgObj.mcpServers) return false;
|
|
835
|
-
// Remove the current key plus the legacy `genexus18` key written by older
|
|
836
|
-
// versions of the build-from-source install.ps1, so uninstall fully cleans up
|
|
837
|
-
// regardless of which installer wrote the entry.
|
|
838
910
|
let removedAny = false;
|
|
839
|
-
|
|
840
|
-
|
|
911
|
+
const keysToRemove = serverName === DEFAULT_MCP_SERVER_NAME
|
|
912
|
+
? [DEFAULT_MCP_SERVER_NAME, 'genexus', 'genexus18']
|
|
913
|
+
: (serverName === 'genexus' ? ['genexus', 'genexus18'] : [serverName]);
|
|
914
|
+
for (const key of keysToRemove) {
|
|
915
|
+
const entry = cfgObj.mcpServers[key];
|
|
916
|
+
if (entry) {
|
|
917
|
+
if (key === 'genexus' && (entry.url || entry.type === 'http' || entry.type === 'sse' || entry.type === 'remote')) {
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
841
920
|
delete cfgObj.mcpServers[key];
|
|
842
921
|
removedAny = true;
|
|
843
922
|
}
|
|
@@ -849,29 +928,50 @@ function removeMcpServersJson(filePath) {
|
|
|
849
928
|
|
|
850
929
|
// VS Code native MCP lives in User\mcp.json and uses a top-level `servers` map
|
|
851
930
|
// with `type: "stdio"` (distinct from the `mcpServers` shape Claude/Cursor use).
|
|
852
|
-
function applyVsCodeServersJson(filePath, launcher, targetConfigPath) {
|
|
931
|
+
function applyVsCodeServersJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false } = {}) {
|
|
853
932
|
const parsed = fs.existsSync(filePath) ? readJsonFileSafe(filePath) : {};
|
|
854
933
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
855
934
|
const cfgObj = parsed || {};
|
|
856
935
|
cfgObj.servers = cfgObj.servers || {};
|
|
857
|
-
cfgObj.servers
|
|
936
|
+
const existing = cfgObj.servers[serverName];
|
|
937
|
+
if (existing && !force && isThirdPartyMcpEntry(existing)) {
|
|
938
|
+
const err = new Error(`Existing '${serverName}' entry is a third-party or HTTP MCP server (e.g. official GeneXus MCP). Refusing to overwrite. Use --server-name=<customName> (e.g. --server-name Gx18byLennix) or --no-write-clients.`);
|
|
939
|
+
err.code = 'MCP_SERVER_COLLISION';
|
|
940
|
+
throw err;
|
|
941
|
+
}
|
|
942
|
+
cfgObj.servers[serverName] = {
|
|
858
943
|
type: 'stdio',
|
|
859
944
|
...launcher,
|
|
860
945
|
env: { GX_CONFIG_PATH: targetConfigPath }
|
|
861
946
|
};
|
|
862
|
-
|
|
863
|
-
|
|
947
|
+
if (serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
948
|
+
if (cfgObj.servers.genexus && !isThirdPartyMcpEntry(cfgObj.servers.genexus)) {
|
|
949
|
+
delete cfgObj.servers.genexus;
|
|
950
|
+
}
|
|
951
|
+
if (cfgObj.servers.genexus18 && !isThirdPartyMcpEntry(cfgObj.servers.genexus18)) {
|
|
952
|
+
delete cfgObj.servers.genexus18;
|
|
953
|
+
}
|
|
954
|
+
} else if (serverName === 'genexus' && cfgObj.servers.genexus18 && !isThirdPartyMcpEntry(cfgObj.servers.genexus18)) {
|
|
955
|
+
delete cfgObj.servers.genexus18;
|
|
956
|
+
}
|
|
864
957
|
writeClientJson(filePath, cfgObj);
|
|
865
958
|
}
|
|
866
959
|
|
|
867
|
-
function removeVsCodeServersJson(filePath) {
|
|
960
|
+
function removeVsCodeServersJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME } = {}) {
|
|
868
961
|
const parsed = readJsonFileSafe(filePath);
|
|
869
962
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
870
963
|
const cfgObj = parsed || {};
|
|
871
964
|
if (!cfgObj.servers) return false;
|
|
872
965
|
let removedAny = false;
|
|
873
|
-
|
|
874
|
-
|
|
966
|
+
const keysToRemove = serverName === DEFAULT_MCP_SERVER_NAME
|
|
967
|
+
? [DEFAULT_MCP_SERVER_NAME, 'genexus', 'genexus18']
|
|
968
|
+
: (serverName === 'genexus' ? ['genexus', 'genexus18'] : [serverName]);
|
|
969
|
+
for (const key of keysToRemove) {
|
|
970
|
+
const entry = cfgObj.servers[key];
|
|
971
|
+
if (entry) {
|
|
972
|
+
if (key === 'genexus' && (entry.url || entry.type === 'http' || entry.type === 'sse' || entry.type === 'remote')) {
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
875
975
|
delete cfgObj.servers[key];
|
|
876
976
|
removedAny = true;
|
|
877
977
|
}
|
|
@@ -898,7 +998,7 @@ function getOpenCodeMcpContainer(cfgObj) {
|
|
|
898
998
|
};
|
|
899
999
|
}
|
|
900
1000
|
|
|
901
|
-
function applyOpenCodeJson(filePath, launcher, targetConfigPath) {
|
|
1001
|
+
function applyOpenCodeJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false } = {}) {
|
|
902
1002
|
const parsed = fs.existsSync(filePath) ? readJsonFileSafe(filePath) : {};
|
|
903
1003
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
904
1004
|
const cfgObj = parsed || {};
|
|
@@ -906,37 +1006,67 @@ function applyOpenCodeJson(filePath, launcher, targetConfigPath) {
|
|
|
906
1006
|
// absent (new file or a config that never had one) without clobbering a custom one.
|
|
907
1007
|
if (!cfgObj.$schema) cfgObj.$schema = 'https://opencode.ai/config.json';
|
|
908
1008
|
const { mcp, servers, nested } = getOpenCodeMcpContainer(cfgObj);
|
|
909
|
-
|
|
1009
|
+
const existing = servers[serverName] || (nested ? mcp[serverName] : null);
|
|
1010
|
+
if (existing && !force && isThirdPartyMcpEntry(existing)) {
|
|
1011
|
+
const err = new Error(`Existing '${serverName}' entry is a third-party or HTTP MCP server (e.g. official GeneXus MCP). Refusing to overwrite. Use --server-name=<customName> (e.g. --server-name Gx18byLennix) or --no-write-clients.`);
|
|
1012
|
+
err.code = 'MCP_SERVER_COLLISION';
|
|
1013
|
+
throw err;
|
|
1014
|
+
}
|
|
1015
|
+
servers[serverName] = {
|
|
910
1016
|
type: 'local',
|
|
911
1017
|
command: [launcher.command, ...(launcher.args || [])],
|
|
912
1018
|
environment: { GX_CONFIG_PATH: targetConfigPath },
|
|
913
1019
|
...(nested ? { disabled: false } : { enabled: true })
|
|
914
1020
|
};
|
|
915
|
-
if (
|
|
1021
|
+
if (serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
1022
|
+
if (servers.genexus && !isThirdPartyMcpEntry(servers.genexus)) delete servers.genexus;
|
|
1023
|
+
if (servers.genexus18 && !isThirdPartyMcpEntry(servers.genexus18)) delete servers.genexus18;
|
|
1024
|
+
} else if (serverName === 'genexus' && servers.genexus18 && !isThirdPartyMcpEntry(servers.genexus18)) {
|
|
1025
|
+
delete servers.genexus18;
|
|
1026
|
+
}
|
|
916
1027
|
// If a config was migrated manually and contains both shapes, leave unrelated
|
|
917
1028
|
// servers alone but remove our duplicate legacy entry.
|
|
918
1029
|
if (nested) {
|
|
919
|
-
if (
|
|
920
|
-
|
|
1030
|
+
if (serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
1031
|
+
if (mcp[DEFAULT_MCP_SERVER_NAME] && !isThirdPartyMcpEntry(mcp[DEFAULT_MCP_SERVER_NAME])) delete mcp[DEFAULT_MCP_SERVER_NAME];
|
|
1032
|
+
if (mcp.genexus && !isThirdPartyMcpEntry(mcp.genexus)) delete mcp.genexus;
|
|
1033
|
+
if (mcp.genexus18 && !isThirdPartyMcpEntry(mcp.genexus18)) delete mcp.genexus18;
|
|
1034
|
+
} else if (serverName === 'genexus') {
|
|
1035
|
+
if (mcp.genexus && !isThirdPartyMcpEntry(mcp.genexus)) delete mcp.genexus;
|
|
1036
|
+
if (mcp.genexus18 && !isThirdPartyMcpEntry(mcp.genexus18)) delete mcp.genexus18;
|
|
1037
|
+
} else {
|
|
1038
|
+
if (mcp[serverName] && !isThirdPartyMcpEntry(mcp[serverName])) delete mcp[serverName];
|
|
1039
|
+
}
|
|
921
1040
|
}
|
|
922
1041
|
writeClientJson(filePath, cfgObj);
|
|
923
1042
|
}
|
|
924
1043
|
|
|
925
|
-
function removeOpenCodeJson(filePath) {
|
|
1044
|
+
function removeOpenCodeJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME } = {}) {
|
|
926
1045
|
const parsed = readJsonFileSafe(filePath);
|
|
927
1046
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
928
1047
|
const cfgObj = parsed || {};
|
|
929
1048
|
if (!cfgObj.mcp || typeof cfgObj.mcp !== 'object') return false;
|
|
930
1049
|
let removedAny = false;
|
|
931
|
-
|
|
932
|
-
|
|
1050
|
+
const keysToRemove = serverName === DEFAULT_MCP_SERVER_NAME
|
|
1051
|
+
? [DEFAULT_MCP_SERVER_NAME, 'genexus', 'genexus18']
|
|
1052
|
+
: (serverName === 'genexus' ? ['genexus', 'genexus18'] : [serverName]);
|
|
1053
|
+
for (const key of keysToRemove) {
|
|
1054
|
+
const entry = cfgObj.mcp[key];
|
|
1055
|
+
if (entry) {
|
|
1056
|
+
if (key === 'genexus' && (entry.url || entry.type === 'http' || entry.type === 'sse' || entry.type === 'remote')) {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
933
1059
|
delete cfgObj.mcp[key];
|
|
934
1060
|
removedAny = true;
|
|
935
1061
|
}
|
|
936
1062
|
}
|
|
937
1063
|
if (cfgObj.mcp.servers && typeof cfgObj.mcp.servers === 'object' && !Array.isArray(cfgObj.mcp.servers)) {
|
|
938
|
-
for (const key of
|
|
939
|
-
|
|
1064
|
+
for (const key of keysToRemove) {
|
|
1065
|
+
const entry = cfgObj.mcp.servers[key];
|
|
1066
|
+
if (entry) {
|
|
1067
|
+
if (key === 'genexus' && (entry.url || entry.type === 'http' || entry.type === 'sse' || entry.type === 'remote')) {
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
940
1070
|
delete cfgObj.mcp.servers[key];
|
|
941
1071
|
removedAny = true;
|
|
942
1072
|
}
|
|
@@ -947,56 +1077,118 @@ function removeOpenCodeJson(filePath) {
|
|
|
947
1077
|
return true;
|
|
948
1078
|
}
|
|
949
1079
|
|
|
1080
|
+
function extractCodexTomlEntry(content, serverName = DEFAULT_MCP_SERVER_NAME) {
|
|
1081
|
+
if (!content) return null;
|
|
1082
|
+
const escaped = escapeRegex(serverName);
|
|
1083
|
+
const blockRe = new RegExp(`\\[mcp_servers\\.${escaped}\\]([\\s\\S]*?)(?=\\n\\[|$)`);
|
|
1084
|
+
const m = content.match(blockRe);
|
|
1085
|
+
if (!m) return null;
|
|
1086
|
+
const cmdMatch = m[1].match(/^\s*command\s*=\s*"((?:[^"\\]|\\.)*)"/m);
|
|
1087
|
+
const urlMatch = m[1].match(/^\s*url\s*=\s*"((?:[^"\\]|\\.)*)"/m);
|
|
1088
|
+
const argsMatch = m[1].match(/^\s*args\s*=\s*\[([\s\S]*?)\]/m);
|
|
1089
|
+
const envMatch = content.match(new RegExp(`\\[mcp_servers\\.${escaped}\\.env\\][\\s\\S]*?GX_CONFIG_PATH\\s*=\\s*"((?:[^"\\\\]|\\\\.)*)"`));
|
|
1090
|
+
const command = cmdMatch ? cmdMatch[1].replace(/\\\\/g, '\\').replace(/\\"/g, '"') : null;
|
|
1091
|
+
const url = urlMatch ? urlMatch[1].replace(/\\\\/g, '\\').replace(/\\"/g, '"') : null;
|
|
1092
|
+
let args = [];
|
|
1093
|
+
if (argsMatch && argsMatch[1]) {
|
|
1094
|
+
const rawArgs = argsMatch[1].match(/"((?:[^"\\]|\\.)*)"/g);
|
|
1095
|
+
if (rawArgs) {
|
|
1096
|
+
args = rawArgs.map((a) => a.slice(1, -1).replace(/\\\\/g, '\\').replace(/\\"/g, '"'));
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const gxConfig = envMatch ? envMatch[1].replace(/\\\\/g, '\\').replace(/\\"/g, '"') : null;
|
|
1100
|
+
return {
|
|
1101
|
+
command,
|
|
1102
|
+
args,
|
|
1103
|
+
url,
|
|
1104
|
+
env: gxConfig ? { GX_CONFIG_PATH: gxConfig } : null
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function stripCodexServerBlocks(content, serverName) {
|
|
1109
|
+
if (!content) return '';
|
|
1110
|
+
const lines = content.split('\n');
|
|
1111
|
+
const out = [];
|
|
1112
|
+
const headerRe = /^\[\[?[A-Za-z0-9_."'-]+/;
|
|
1113
|
+
const ourRe = new RegExp(`^\\[\\[?mcp_servers\\.${escapeRegex(serverName)}(\\.[A-Za-z0-9_."'-]+)?\\]\\]?\\s*$`);
|
|
1114
|
+
let inOurs = false;
|
|
1115
|
+
for (const line of lines) {
|
|
1116
|
+
if (headerRe.test(line)) {
|
|
1117
|
+
inOurs = ourRe.test(line);
|
|
1118
|
+
if (inOurs) continue;
|
|
1119
|
+
}
|
|
1120
|
+
if (inOurs) continue;
|
|
1121
|
+
out.push(line);
|
|
1122
|
+
}
|
|
1123
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
1124
|
+
}
|
|
1125
|
+
|
|
950
1126
|
// Codex CLI uses TOML. We do a minimal text-merge: strip any existing
|
|
951
1127
|
// [mcp_servers.genexus*] blocks and append fresh ones. Brittle on hand-edited
|
|
952
1128
|
// files that put other keys after our blocks without a blank line, but
|
|
953
1129
|
// adequate for the typical machine-managed config.
|
|
954
|
-
function applyCodexToml(filePath, launcher, targetConfigPath) {
|
|
1130
|
+
function applyCodexToml(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false } = {}) {
|
|
955
1131
|
let existing = '';
|
|
956
1132
|
if (fs.existsSync(filePath)) existing = fs.readFileSync(filePath, 'utf8');
|
|
957
|
-
const
|
|
1133
|
+
const existingEntry = extractCodexTomlEntry(existing, serverName);
|
|
1134
|
+
if (existingEntry && !force && isThirdPartyMcpEntry(existingEntry)) {
|
|
1135
|
+
const err = new Error(`Existing '${serverName}' entry is a third-party or HTTP MCP server (e.g. official GeneXus MCP). Refusing to overwrite. Use --server-name=<customName> (e.g. --server-name Gx18byLennix) or --no-write-clients.`);
|
|
1136
|
+
err.code = 'MCP_SERVER_COLLISION';
|
|
1137
|
+
throw err;
|
|
1138
|
+
}
|
|
1139
|
+
let stripped = stripCodexServerBlocks(existing, serverName);
|
|
1140
|
+
if (serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
1141
|
+
const legacyGx = extractCodexTomlEntry(stripped, 'genexus');
|
|
1142
|
+
if (legacyGx && !isThirdPartyMcpEntry(legacyGx)) {
|
|
1143
|
+
stripped = stripCodexServerBlocks(stripped, 'genexus');
|
|
1144
|
+
}
|
|
1145
|
+
const legacyGx18 = extractCodexTomlEntry(stripped, 'genexus18');
|
|
1146
|
+
if (legacyGx18 && !isThirdPartyMcpEntry(legacyGx18)) {
|
|
1147
|
+
stripped = stripCodexServerBlocks(stripped, 'genexus18');
|
|
1148
|
+
}
|
|
1149
|
+
} else if (serverName === 'genexus') {
|
|
1150
|
+
const legacyEntry = extractCodexTomlEntry(stripped, 'genexus18');
|
|
1151
|
+
if (legacyEntry && !isThirdPartyMcpEntry(legacyEntry)) {
|
|
1152
|
+
stripped = stripCodexServerBlocks(stripped, 'genexus18');
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
958
1155
|
const args = launcher.args || [];
|
|
959
1156
|
const lines = [];
|
|
960
1157
|
if (stripped.length && !stripped.endsWith('\n')) lines.push('');
|
|
961
1158
|
if (stripped.length) lines.push('');
|
|
962
|
-
lines.push(
|
|
1159
|
+
lines.push(`[mcp_servers.${serverName}]`);
|
|
963
1160
|
lines.push(`command = ${tomlString(launcher.command)}`);
|
|
964
1161
|
lines.push(`args = [${args.map(tomlString).join(', ')}]`);
|
|
965
1162
|
lines.push('');
|
|
966
|
-
lines.push(
|
|
1163
|
+
lines.push(`[mcp_servers.${serverName}.env]`);
|
|
967
1164
|
lines.push(`GX_CONFIG_PATH = ${tomlString(targetConfigPath)}`);
|
|
968
1165
|
lines.push('');
|
|
969
1166
|
writeClientText(filePath, stripped + lines.join('\n'));
|
|
970
1167
|
}
|
|
971
1168
|
|
|
972
|
-
function removeCodexToml(filePath) {
|
|
1169
|
+
function removeCodexToml(filePath, { serverName = DEFAULT_MCP_SERVER_NAME } = {}) {
|
|
973
1170
|
if (!fs.existsSync(filePath)) return false;
|
|
974
1171
|
const existing = fs.readFileSync(filePath, 'utf8');
|
|
975
|
-
const
|
|
1172
|
+
const keysToRemove = serverName === DEFAULT_MCP_SERVER_NAME
|
|
1173
|
+
? [DEFAULT_MCP_SERVER_NAME, 'genexus', 'genexus18']
|
|
1174
|
+
: (serverName === 'genexus' ? ['genexus', 'genexus18'] : [serverName]);
|
|
1175
|
+
let stripped = existing;
|
|
1176
|
+
for (const key of keysToRemove) {
|
|
1177
|
+
const entry = extractCodexTomlEntry(stripped, key);
|
|
1178
|
+
if (entry) {
|
|
1179
|
+
if (key === 'genexus' && (entry.url || entry.type === 'http' || entry.type === 'sse' || entry.type === 'remote')) {
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
stripped = stripCodexServerBlocks(stripped, key);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
976
1185
|
if (stripped === existing) return false;
|
|
977
1186
|
writeClientText(filePath, stripped);
|
|
978
1187
|
return true;
|
|
979
1188
|
}
|
|
980
1189
|
|
|
981
1190
|
function stripCodexGenexusBlocks(content) {
|
|
982
|
-
|
|
983
|
-
// A section ends at the next line that starts a [section] header (top-level
|
|
984
|
-
// tables and arrays of tables only — must begin at column 0).
|
|
985
|
-
const lines = content.split('\n');
|
|
986
|
-
const out = [];
|
|
987
|
-
const headerRe = /^\[\[?[A-Za-z0-9_."'-]+/;
|
|
988
|
-
const ourRe = /^\[\[?mcp_servers\.genexus(\.[A-Za-z0-9_."'-]+)?\]\]?\s*$/;
|
|
989
|
-
let inOurs = false;
|
|
990
|
-
for (const line of lines) {
|
|
991
|
-
if (headerRe.test(line)) {
|
|
992
|
-
inOurs = ourRe.test(line);
|
|
993
|
-
if (inOurs) continue;
|
|
994
|
-
}
|
|
995
|
-
if (inOurs) continue;
|
|
996
|
-
out.push(line);
|
|
997
|
-
}
|
|
998
|
-
// Collapse 3+ trailing blank lines we may have left behind.
|
|
999
|
-
return out.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
1191
|
+
return stripCodexServerBlocks(content, 'genexus');
|
|
1000
1192
|
}
|
|
1001
1193
|
|
|
1002
1194
|
function tomlString(value) {
|
|
@@ -1029,41 +1221,94 @@ function normalizeExePath(p) {
|
|
|
1029
1221
|
return s;
|
|
1030
1222
|
}
|
|
1031
1223
|
|
|
1032
|
-
function readClientCommandEntry(client) {
|
|
1224
|
+
function readClientCommandEntry(client, serverName = DEFAULT_MCP_SERVER_NAME) {
|
|
1033
1225
|
if (client.writeSupported === false) return null;
|
|
1034
1226
|
if (!fs.existsSync(client.path)) return null;
|
|
1035
1227
|
try {
|
|
1036
1228
|
if (client.format === 'mcpServers') {
|
|
1037
1229
|
const parsed = readJsonFileSafe(client.path);
|
|
1038
1230
|
if (!parsed || typeof parsed !== 'object') return null;
|
|
1039
|
-
|
|
1231
|
+
let entry = parsed.mcpServers && parsed.mcpServers[serverName];
|
|
1232
|
+
if (!entry && serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
1233
|
+
const legacyGx = parsed.mcpServers && parsed.mcpServers.genexus;
|
|
1234
|
+
if (legacyGx && !isThirdPartyMcpEntry(legacyGx)) entry = legacyGx;
|
|
1235
|
+
else if (parsed.mcpServers && parsed.mcpServers.genexus18) entry = parsed.mcpServers.genexus18;
|
|
1236
|
+
}
|
|
1040
1237
|
if (!entry) return null;
|
|
1041
|
-
return {
|
|
1238
|
+
return {
|
|
1239
|
+
command: entry.command || null,
|
|
1240
|
+
args: Array.isArray(entry.args) ? entry.args : [],
|
|
1241
|
+
url: entry.url || null,
|
|
1242
|
+
type: entry.type || null,
|
|
1243
|
+
env: entry.env || null,
|
|
1244
|
+
raw: entry
|
|
1245
|
+
};
|
|
1042
1246
|
}
|
|
1043
1247
|
if (client.format === 'opencode') {
|
|
1044
1248
|
const parsed = readJsonFileSafe(client.path);
|
|
1045
1249
|
if (!parsed || typeof parsed !== 'object') return null;
|
|
1046
|
-
|
|
1047
|
-
if (!entry
|
|
1048
|
-
|
|
1250
|
+
let entry = parsed.mcp?.servers?.[serverName] || parsed.mcp?.[serverName];
|
|
1251
|
+
if (!entry && serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
1252
|
+
const legacyGx = parsed.mcp?.servers?.genexus || parsed.mcp?.genexus;
|
|
1253
|
+
if (legacyGx && !isThirdPartyMcpEntry(legacyGx)) entry = legacyGx;
|
|
1254
|
+
else entry = parsed.mcp?.servers?.genexus18 || parsed.mcp?.genexus18;
|
|
1255
|
+
}
|
|
1256
|
+
if (!entry) return null;
|
|
1257
|
+
if (Array.isArray(entry.command) && entry.command.length > 0) {
|
|
1258
|
+
return {
|
|
1259
|
+
command: entry.command[0],
|
|
1260
|
+
args: entry.command.slice(1),
|
|
1261
|
+
url: entry.url || null,
|
|
1262
|
+
type: entry.type || null,
|
|
1263
|
+
environment: entry.environment || null,
|
|
1264
|
+
raw: entry
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
return {
|
|
1268
|
+
command: null,
|
|
1269
|
+
args: [],
|
|
1270
|
+
url: entry.url || null,
|
|
1271
|
+
type: entry.type || null,
|
|
1272
|
+
environment: entry.environment || null,
|
|
1273
|
+
raw: entry
|
|
1274
|
+
};
|
|
1049
1275
|
}
|
|
1050
1276
|
if (client.format === 'vscode-servers') {
|
|
1051
1277
|
const parsed = readJsonFileSafe(client.path);
|
|
1052
1278
|
if (!parsed || typeof parsed !== 'object') return null;
|
|
1053
|
-
|
|
1279
|
+
let entry = parsed.servers && parsed.servers[serverName];
|
|
1280
|
+
if (!entry && serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
1281
|
+
const legacyGx = parsed.servers && parsed.servers.genexus;
|
|
1282
|
+
if (legacyGx && !isThirdPartyMcpEntry(legacyGx)) entry = legacyGx;
|
|
1283
|
+
else if (parsed.servers && parsed.servers.genexus18) entry = parsed.servers.genexus18;
|
|
1284
|
+
}
|
|
1054
1285
|
if (!entry) return null;
|
|
1055
|
-
return {
|
|
1286
|
+
return {
|
|
1287
|
+
command: entry.command || null,
|
|
1288
|
+
args: Array.isArray(entry.args) ? entry.args : [],
|
|
1289
|
+
url: entry.url || null,
|
|
1290
|
+
type: entry.type || null,
|
|
1291
|
+
env: entry.env || null,
|
|
1292
|
+
raw: entry
|
|
1293
|
+
};
|
|
1056
1294
|
}
|
|
1057
1295
|
if (client.format === 'codex-toml') {
|
|
1058
1296
|
const raw = fs.readFileSync(client.path, 'utf8');
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
return {
|
|
1297
|
+
let entry = extractCodexTomlEntry(raw, serverName);
|
|
1298
|
+
if (!entry && serverName === DEFAULT_MCP_SERVER_NAME) {
|
|
1299
|
+
const legacyGx = extractCodexTomlEntry(raw, 'genexus');
|
|
1300
|
+
if (legacyGx && !isThirdPartyMcpEntry(legacyGx)) entry = legacyGx;
|
|
1301
|
+
else entry = extractCodexTomlEntry(raw, 'genexus18');
|
|
1302
|
+
}
|
|
1303
|
+
if (!entry) return null;
|
|
1304
|
+
return {
|
|
1305
|
+
command: entry.command || null,
|
|
1306
|
+
args: entry.args || [],
|
|
1307
|
+
url: entry.url || null,
|
|
1308
|
+
type: entry.type || null,
|
|
1309
|
+
env: entry.env || null,
|
|
1310
|
+
raw: entry
|
|
1311
|
+
};
|
|
1067
1312
|
}
|
|
1068
1313
|
} catch {
|
|
1069
1314
|
return null;
|
|
@@ -1274,5 +1519,7 @@ module.exports = {
|
|
|
1274
1519
|
applyLauncherConfigOrExit,
|
|
1275
1520
|
isPathLikelyAppLockerBlocked,
|
|
1276
1521
|
normalizeExePath,
|
|
1277
|
-
readClientCommandEntry
|
|
1522
|
+
readClientCommandEntry,
|
|
1523
|
+
isOurMcpEntry,
|
|
1524
|
+
DEFAULT_MCP_SERVER_NAME
|
|
1278
1525
|
};
|
package/cli/run.test.js
CHANGED
|
@@ -757,7 +757,7 @@ test('clients add registers a client into a sandbox home with backup + atomic wr
|
|
|
757
757
|
assert.ok(parsed.ok.patchedClients.includes('Cursor'));
|
|
758
758
|
|
|
759
759
|
const written = JSON.parse(fs.readFileSync(cursorCfg, 'utf8'));
|
|
760
|
-
assert.ok(written.mcpServers.
|
|
760
|
+
assert.ok(written.mcpServers.genexus18mcp, 'genexus18mcp entry should be written');
|
|
761
761
|
assert.ok(written.mcpServers.other, 'pre-existing entries preserved');
|
|
762
762
|
|
|
763
763
|
const baks = fs.readdirSync(path.dirname(cursorCfg)).filter((f) => f.includes('.bak'));
|
|
@@ -784,7 +784,7 @@ test('clients add tolerates a JSONC (commented) VS Code mcp.json', () => {
|
|
|
784
784
|
assert.ok(parsed.ok.patchedClients.includes('VS Code'), 'VS Code should be patched despite comments');
|
|
785
785
|
|
|
786
786
|
const written = JSON.parse(fs.readFileSync(vscodeCfg, 'utf8'));
|
|
787
|
-
assert.ok(written.servers.
|
|
787
|
+
assert.ok(written.servers.genexus18mcp, 'genexus18mcp server entry written');
|
|
788
788
|
assert.ok(written.servers.foo, 'pre-existing server preserved');
|
|
789
789
|
|
|
790
790
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
@@ -806,12 +806,12 @@ test('clients add preserves OpenCode 1.x direct mcp shape', () => {
|
|
|
806
806
|
});
|
|
807
807
|
assert.equal(res.status, 0);
|
|
808
808
|
const written = JSON.parse(fs.readFileSync(openCodeCfg, 'utf8'));
|
|
809
|
-
assert.ok(written.mcp.
|
|
810
|
-
assert.equal(written.mcp.
|
|
811
|
-
assert.equal(written.mcp.
|
|
809
|
+
assert.ok(written.mcp.genexus18mcp, 'direct OpenCode entry should be written');
|
|
810
|
+
assert.equal(written.mcp.genexus18mcp.enabled, true);
|
|
811
|
+
assert.equal(written.mcp.genexus18mcp.disabled, undefined);
|
|
812
812
|
assert.ok(written.mcp.other, 'unrelated direct MCP server should be preserved');
|
|
813
813
|
assert.equal(written.mcp.servers, undefined);
|
|
814
|
-
assert.deepEqual(written.mcp.
|
|
814
|
+
assert.deepEqual(written.mcp.genexus18mcp.environment, { GX_CONFIG_PATH: cfgPath });
|
|
815
815
|
|
|
816
816
|
const listed = runCli(['clients', '--format', 'json'], { env });
|
|
817
817
|
assert.equal(listed.status, 0);
|
|
@@ -837,12 +837,12 @@ test('clients add preserves OpenCode v2 nested mcp.servers shape', () => {
|
|
|
837
837
|
});
|
|
838
838
|
assert.equal(res.status, 0);
|
|
839
839
|
const written = JSON.parse(fs.readFileSync(openCodeCfg, 'utf8'));
|
|
840
|
-
assert.ok(written.mcp.servers.
|
|
841
|
-
assert.equal(written.mcp.servers.
|
|
842
|
-
assert.equal(written.mcp.servers.
|
|
840
|
+
assert.ok(written.mcp.servers.genexus18mcp, 'nested OpenCode entry should be written');
|
|
841
|
+
assert.equal(written.mcp.servers.genexus18mcp.disabled, false);
|
|
842
|
+
assert.equal(written.mcp.servers.genexus18mcp.enabled, undefined);
|
|
843
843
|
assert.ok(written.mcp.servers.other, 'unrelated nested MCP server should be preserved');
|
|
844
|
-
assert.equal(written.mcp.
|
|
845
|
-
assert.deepEqual(written.mcp.servers.
|
|
844
|
+
assert.equal(written.mcp.genexus18mcp, undefined);
|
|
845
|
+
assert.deepEqual(written.mcp.servers.genexus18mcp.environment, { GX_CONFIG_PATH: cfgPath });
|
|
846
846
|
|
|
847
847
|
const listed = runCli(['clients', '--format', 'json'], { env });
|
|
848
848
|
assert.equal(listed.status, 0);
|
|
@@ -877,7 +877,7 @@ test('init auto-registers detected OpenCode in either config layout', () => {
|
|
|
877
877
|
assert.ok(parsed.meta.patchedClients.includes('OpenCode (CLI)'));
|
|
878
878
|
|
|
879
879
|
const written = JSON.parse(fs.readFileSync(openCodeCfg, 'utf8'));
|
|
880
|
-
const entry = label === 'nested' ? written.mcp.servers.
|
|
880
|
+
const entry = label === 'nested' ? written.mcp.servers.genexus18mcp : written.mcp.genexus18mcp;
|
|
881
881
|
assert.ok(entry, `${label} OpenCode entry should be present after init`);
|
|
882
882
|
assert.deepEqual(entry.environment, { GX_CONFIG_PATH: path.join(kbDir, 'config.json') });
|
|
883
883
|
assert.ok(label === 'nested' ? written.mcp.servers.other : written.mcp.other);
|
|
@@ -903,7 +903,7 @@ test('clients add replaces a legacy genexus18 entry instead of duplicating it',
|
|
|
903
903
|
assert.equal(res.status, 0);
|
|
904
904
|
|
|
905
905
|
const written = JSON.parse(fs.readFileSync(cursorCfg, 'utf8'));
|
|
906
|
-
assert.ok(written.mcpServers.
|
|
906
|
+
assert.ok(written.mcpServers.genexus18mcp, 'new genexus18mcp entry present');
|
|
907
907
|
assert.equal(written.mcpServers.genexus18, undefined, 'legacy genexus18 removed (no duplicate)');
|
|
908
908
|
|
|
909
909
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
@@ -916,7 +916,7 @@ test('clients list flags a registered command pointing at a missing launcher as
|
|
|
916
916
|
fs.mkdirSync(path.dirname(cursorCfg), { recursive: true });
|
|
917
917
|
// A non-.exe launcher (.bat) that no longer exists must also be flagged stale.
|
|
918
918
|
const missing = path.join(tempRoot, 'gone', 'start_mcp.bat');
|
|
919
|
-
fs.writeFileSync(cursorCfg, JSON.stringify({ mcpServers: {
|
|
919
|
+
fs.writeFileSync(cursorCfg, JSON.stringify({ mcpServers: { genexus18mcp: { command: missing, args: [] } } }, null, 2));
|
|
920
920
|
|
|
921
921
|
const res = runCli(['clients', '--format', 'json'], { env });
|
|
922
922
|
assert.equal(res.status, 0);
|
|
@@ -964,7 +964,7 @@ test('clients remove drops the genexus entry (sandbox home)', () => {
|
|
|
964
964
|
const env = sandboxHomeEnv(tempRoot);
|
|
965
965
|
const cursorCfg = path.join(tempRoot, '.cursor', 'mcp.json');
|
|
966
966
|
fs.mkdirSync(path.dirname(cursorCfg), { recursive: true });
|
|
967
|
-
fs.writeFileSync(cursorCfg, JSON.stringify({ mcpServers: {
|
|
967
|
+
fs.writeFileSync(cursorCfg, JSON.stringify({ mcpServers: { genexus18mcp: { command: 'npx' }, genexus18: { command: 'old' } } }, null, 2));
|
|
968
968
|
|
|
969
969
|
const res = runCli(['clients', 'remove', '--clients', 'cursor', '--format', 'json'], { env });
|
|
970
970
|
assert.equal(res.status, 0);
|
|
@@ -972,7 +972,7 @@ test('clients remove drops the genexus entry (sandbox home)', () => {
|
|
|
972
972
|
assert.ok(parsed.ok.removedClients.includes('Cursor'));
|
|
973
973
|
|
|
974
974
|
const written = JSON.parse(fs.readFileSync(cursorCfg, 'utf8'));
|
|
975
|
-
assert.equal(written.mcpServers.
|
|
975
|
+
assert.equal(written.mcpServers.genexus18mcp, undefined, 'genexus18mcp removed');
|
|
976
976
|
assert.equal(written.mcpServers.genexus18, undefined, 'legacy genexus18 also removed');
|
|
977
977
|
|
|
978
978
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
@@ -987,9 +987,11 @@ test('clients remove drops both OpenCode config shapes and legacy key', () => {
|
|
|
987
987
|
mcp: {
|
|
988
988
|
genexus: { type: 'local', command: ['old'] },
|
|
989
989
|
genexus18: { type: 'local', command: ['older'] },
|
|
990
|
+
genexus18mcp: { type: 'local', command: ['latest'] },
|
|
990
991
|
servers: {
|
|
991
992
|
genexus: { type: 'local', command: ['nested'] },
|
|
992
993
|
genexus18: { type: 'local', command: ['nested-old'] },
|
|
994
|
+
genexus18mcp: { type: 'local', command: ['nested-latest'] },
|
|
993
995
|
other: { type: 'local', command: ['other'] }
|
|
994
996
|
}
|
|
995
997
|
}
|
|
@@ -1000,8 +1002,10 @@ test('clients remove drops both OpenCode config shapes and legacy key', () => {
|
|
|
1000
1002
|
const written = JSON.parse(fs.readFileSync(openCodeCfg, 'utf8'));
|
|
1001
1003
|
assert.equal(written.mcp.genexus, undefined);
|
|
1002
1004
|
assert.equal(written.mcp.genexus18, undefined);
|
|
1005
|
+
assert.equal(written.mcp.genexus18mcp, undefined);
|
|
1003
1006
|
assert.equal(written.mcp.servers.genexus, undefined);
|
|
1004
1007
|
assert.equal(written.mcp.servers.genexus18, undefined);
|
|
1008
|
+
assert.equal(written.mcp.servers.genexus18mcp, undefined);
|
|
1005
1009
|
assert.ok(written.mcp.servers.other, 'unrelated nested MCP server should be preserved');
|
|
1006
1010
|
|
|
1007
1011
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
@@ -1096,3 +1100,146 @@ test('npm package contains all bin, postinstall and required runtime files', ()
|
|
|
1096
1100
|
}
|
|
1097
1101
|
}
|
|
1098
1102
|
});
|
|
1103
|
+
|
|
1104
|
+
test('isOurMcpEntry identifies third-party / HTTP servers vs our own entry', () => {
|
|
1105
|
+
const { isOurMcpEntry } = require('./lib/config');
|
|
1106
|
+
|
|
1107
|
+
// Official / third-party MCP servers
|
|
1108
|
+
assert.equal(isOurMcpEntry({ url: 'http://localhost:8001/mcp' }), false);
|
|
1109
|
+
assert.equal(isOurMcpEntry({ type: 'http', url: 'https://example.com/mcp' }), false);
|
|
1110
|
+
assert.equal(isOurMcpEntry({ type: 'sse', url: 'http://127.0.0.1:8000/sse' }), false);
|
|
1111
|
+
assert.equal(isOurMcpEntry({ command: 'node', args: ['some-other-server.js'] }), false);
|
|
1112
|
+
|
|
1113
|
+
// Our entries
|
|
1114
|
+
assert.equal(isOurMcpEntry({ command: 'npx.cmd', args: ['-y', 'genexus-mcp@latest'] }), true);
|
|
1115
|
+
assert.equal(isOurMcpEntry({ command: 'C:\\path\\start_mcp.bat' }), true);
|
|
1116
|
+
assert.equal(isOurMcpEntry({ command: 'C:\\bin\\GxMcp.Gateway.exe' }), true);
|
|
1117
|
+
assert.equal(isOurMcpEntry({ command: 'node', env: { GX_CONFIG_PATH: 'C:\\kb\\config.json' } }), true);
|
|
1118
|
+
assert.equal(isOurMcpEntry({ url: 'http://custom' }, 'genexus18mcp'), true);
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
test('clients add refuses to overwrite a third-party HTTP MCP server without --force', () => {
|
|
1122
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-collision-'));
|
|
1123
|
+
const env = sandboxHomeEnv(tempRoot);
|
|
1124
|
+
const cfgPath = path.join(tempRoot, 'config.json');
|
|
1125
|
+
fs.writeFileSync(cfgPath, JSON.stringify({ Environment: { KBPath: tempRoot } }));
|
|
1126
|
+
|
|
1127
|
+
// Pre-existing official GeneXus MCP entry under genexus18mcp
|
|
1128
|
+
const cursorCfg = path.join(tempRoot, '.cursor', 'mcp.json');
|
|
1129
|
+
fs.mkdirSync(path.dirname(cursorCfg), { recursive: true });
|
|
1130
|
+
fs.writeFileSync(cursorCfg, JSON.stringify({
|
|
1131
|
+
mcpServers: {
|
|
1132
|
+
genexus18mcp: { url: 'http://localhost:8001/mcp', type: 'http' }
|
|
1133
|
+
}
|
|
1134
|
+
}, null, 2));
|
|
1135
|
+
|
|
1136
|
+
const res = runCli(['clients', 'add', '--clients', 'cursor', '--format', 'json'], {
|
|
1137
|
+
env: { ...env, GX_CONFIG_PATH: cfgPath }
|
|
1138
|
+
});
|
|
1139
|
+
assert.equal(res.status, 0);
|
|
1140
|
+
const parsed = JSON.parse(res.stdout);
|
|
1141
|
+
assert.equal(parsed.ok.patchedClients.length, 0);
|
|
1142
|
+
assert.equal(parsed.meta.failedClients.length, 1);
|
|
1143
|
+
assert.match(parsed.meta.failedClients[0].reason, /third-party or HTTP MCP server/);
|
|
1144
|
+
|
|
1145
|
+
// Ensure the original HTTP server was NOT modified or deleted
|
|
1146
|
+
const after = JSON.parse(fs.readFileSync(cursorCfg, 'utf8'));
|
|
1147
|
+
assert.equal(after.mcpServers.genexus18mcp.url, 'http://localhost:8001/mcp');
|
|
1148
|
+
|
|
1149
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
test('clients add with --force overwrites an existing third-party entry', () => {
|
|
1153
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-force-'));
|
|
1154
|
+
const env = sandboxHomeEnv(tempRoot);
|
|
1155
|
+
const cfgPath = path.join(tempRoot, 'config.json');
|
|
1156
|
+
fs.writeFileSync(cfgPath, JSON.stringify({ Environment: { KBPath: tempRoot } }));
|
|
1157
|
+
|
|
1158
|
+
const cursorCfg = path.join(tempRoot, '.cursor', 'mcp.json');
|
|
1159
|
+
fs.mkdirSync(path.dirname(cursorCfg), { recursive: true });
|
|
1160
|
+
fs.writeFileSync(cursorCfg, JSON.stringify({
|
|
1161
|
+
mcpServers: {
|
|
1162
|
+
genexus18mcp: { url: 'http://localhost:8001/mcp', type: 'http' }
|
|
1163
|
+
}
|
|
1164
|
+
}, null, 2));
|
|
1165
|
+
|
|
1166
|
+
const res = runCli(['clients', 'add', '--clients', 'cursor', '--force', '--format', 'json'], {
|
|
1167
|
+
env: { ...env, GX_CONFIG_PATH: cfgPath }
|
|
1168
|
+
});
|
|
1169
|
+
assert.equal(res.status, 0);
|
|
1170
|
+
const parsed = JSON.parse(res.stdout);
|
|
1171
|
+
assert.ok(parsed.ok.patchedClients.includes('Cursor'));
|
|
1172
|
+
|
|
1173
|
+
const after = JSON.parse(fs.readFileSync(cursorCfg, 'utf8'));
|
|
1174
|
+
assert.equal(after.mcpServers.genexus18mcp.url, undefined);
|
|
1175
|
+
assert.ok(after.mcpServers.genexus18mcp.command);
|
|
1176
|
+
|
|
1177
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
test('clients add supports custom --server-name across all formats for multi-MCP coexistence', () => {
|
|
1181
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-custom-name-'));
|
|
1182
|
+
const env = sandboxHomeEnv(tempRoot);
|
|
1183
|
+
const cfgPath = path.join(tempRoot, 'config.json');
|
|
1184
|
+
fs.writeFileSync(cfgPath, JSON.stringify({ Environment: { KBPath: tempRoot } }));
|
|
1185
|
+
|
|
1186
|
+
// 1. mcpServers (Cursor)
|
|
1187
|
+
const cursorCfg = path.join(tempRoot, '.cursor', 'mcp.json');
|
|
1188
|
+
fs.mkdirSync(path.dirname(cursorCfg), { recursive: true });
|
|
1189
|
+
fs.writeFileSync(cursorCfg, JSON.stringify({
|
|
1190
|
+
mcpServers: { genexus: { url: 'http://localhost:8001/mcp' } }
|
|
1191
|
+
}, null, 2));
|
|
1192
|
+
|
|
1193
|
+
// 2. VS Code servers
|
|
1194
|
+
const vscodeCfg = path.join(env.APPDATA, 'Code', 'User', 'mcp.json');
|
|
1195
|
+
fs.mkdirSync(path.dirname(vscodeCfg), { recursive: true });
|
|
1196
|
+
fs.writeFileSync(vscodeCfg, JSON.stringify({
|
|
1197
|
+
servers: { genexus: { url: 'http://localhost:8001/mcp' } }
|
|
1198
|
+
}, null, 2));
|
|
1199
|
+
|
|
1200
|
+
// 3. OpenCode nested
|
|
1201
|
+
const openCodeCfg = path.join(env.XDG_CONFIG_HOME, 'opencode', 'opencode.json');
|
|
1202
|
+
fs.mkdirSync(path.dirname(openCodeCfg), { recursive: true });
|
|
1203
|
+
fs.writeFileSync(openCodeCfg, JSON.stringify({
|
|
1204
|
+
mcp: { servers: { genexus: { url: 'http://localhost:8001/mcp' } } }
|
|
1205
|
+
}, null, 2));
|
|
1206
|
+
|
|
1207
|
+
// 4. Codex TOML
|
|
1208
|
+
const codexCfg = path.join(tempRoot, '.codex', 'config.toml');
|
|
1209
|
+
fs.mkdirSync(path.dirname(codexCfg), { recursive: true });
|
|
1210
|
+
fs.writeFileSync(codexCfg, '[mcp_servers.genexus]\nurl = "http://localhost:8001/mcp"\n');
|
|
1211
|
+
|
|
1212
|
+
const res = runCli(['clients', 'add', '--clients', 'cursor,vscode,opencode,codex-cli', '--server-name', 'Gx18byLennix', '--format', 'json'], {
|
|
1213
|
+
env: { ...env, GX_CONFIG_PATH: cfgPath }
|
|
1214
|
+
});
|
|
1215
|
+
assert.equal(res.status, 0);
|
|
1216
|
+
const parsed = JSON.parse(res.stdout);
|
|
1217
|
+
assert.equal(parsed.ok.patchedCount, 4);
|
|
1218
|
+
|
|
1219
|
+
// Assert both coexist side-by-side
|
|
1220
|
+
const cursorWritten = JSON.parse(fs.readFileSync(cursorCfg, 'utf8'));
|
|
1221
|
+
assert.equal(cursorWritten.mcpServers.genexus.url, 'http://localhost:8001/mcp');
|
|
1222
|
+
assert.ok(cursorWritten.mcpServers.Gx18byLennix);
|
|
1223
|
+
|
|
1224
|
+
const vsCodeWritten = JSON.parse(fs.readFileSync(vscodeCfg, 'utf8'));
|
|
1225
|
+
assert.equal(vsCodeWritten.servers.genexus.url, 'http://localhost:8001/mcp');
|
|
1226
|
+
assert.ok(vsCodeWritten.servers.Gx18byLennix);
|
|
1227
|
+
|
|
1228
|
+
const openCodeWritten = JSON.parse(fs.readFileSync(openCodeCfg, 'utf8'));
|
|
1229
|
+
assert.equal(openCodeWritten.mcp.servers.genexus.url, 'http://localhost:8001/mcp');
|
|
1230
|
+
assert.ok(openCodeWritten.mcp.servers.Gx18byLennix);
|
|
1231
|
+
|
|
1232
|
+
const codexWritten = fs.readFileSync(codexCfg, 'utf8');
|
|
1233
|
+
assert.ok(codexWritten.includes('[mcp_servers.genexus]'));
|
|
1234
|
+
assert.ok(codexWritten.includes('[mcp_servers.Gx18byLennix]'));
|
|
1235
|
+
|
|
1236
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
test('clients add rejects invalid --server-name characters with usage error', () => {
|
|
1240
|
+
const res = runCli(['clients', 'add', '--clients', 'cursor', '--server-name', 'bad name with spaces!', '--format', 'json']);
|
|
1241
|
+
assert.equal(res.status, 2);
|
|
1242
|
+
const parsed = JSON.parse(res.stdout);
|
|
1243
|
+
assert.equal(parsed.error.code, 'usage_error');
|
|
1244
|
+
assert.match(parsed.error.message, /alphanumeric/);
|
|
1245
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genexus-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.49.1",
|
|
4
4
|
"mcpName": "io.github.lennix1337/genexus",
|
|
5
5
|
"description": "GeneXus 18 MCP server — read, edit, and analyze GeneXus knowledge base objects (transactions, web panels, procedures, SDTs) directly from Claude, Cursor, and other AI agents over the Model Context Protocol.",
|
|
6
6
|
"keywords": [
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"targets": {
|
|
8
8
|
".NETCoreApp,Version=v8.0": {},
|
|
9
9
|
".NETCoreApp,Version=v8.0/win-x64": {
|
|
10
|
-
"GxMcp.Gateway/2.
|
|
10
|
+
"GxMcp.Gateway/2.49.1": {
|
|
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/2.
|
|
69
|
+
"GxMcp.Gateway/2.49.1": {
|
|
70
70
|
"type": "project",
|
|
71
71
|
"serviceable": false,
|
|
72
72
|
"sha512": ""
|
|
Binary file
|
|
Binary file
|
package/publish/config.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
+
"GeneXus": {
|
|
3
|
+
"InstallationPath": "C:\\\\Program Files (x86)\\\\GeneXus\\\\GeneXus18",
|
|
4
|
+
"WorkerExecutable": "C:\\Projetos\\Genexus18MCP\\publish\\\\worker\\\\GxMcp.Worker.exe"
|
|
5
|
+
},
|
|
2
6
|
"Environment": {
|
|
3
7
|
"KBPath": "C:\\\\KBs\\\\YourKB"
|
|
4
8
|
},
|
|
5
9
|
"Logging": {
|
|
6
|
-
"
|
|
7
|
-
"
|
|
10
|
+
"Path": "logs",
|
|
11
|
+
"Level": "Debug"
|
|
8
12
|
},
|
|
9
13
|
"Server": {
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
},
|
|
13
|
-
"GeneXus": {
|
|
14
|
-
"WorkerExecutable": "C:\\Projetos\\Genexus18MCP\\publish\\\\worker\\\\GxMcp.Worker.exe",
|
|
15
|
-
"InstallationPath": "C:\\\\Program Files (x86)\\\\GeneXus\\\\GeneXus18"
|
|
14
|
+
"HttpPort": 5000,
|
|
15
|
+
"McpStdio": true
|
|
16
16
|
}
|
|
17
17
|
}
|
|
Binary file
|