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.
@@ -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, { ids, onlyExisting: false });
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.length > 0) help.push('Some clients failed (see meta.failedClients).');
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(ids ? { ids } : {});
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 remove --clients cursor'
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('post-write verification failed (genexus entry not found after write)');
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: 'no genexus entry' });
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.genexus = { ...launcher, env: { GX_CONFIG_PATH: targetConfigPath } };
824
- // Drop the legacy `genexus18` key from older build-from-source installs so the
825
- // user isn't left with two duplicate servers (and colliding tool names).
826
- if (cfgObj.mcpServers.genexus18) delete cfgObj.mcpServers.genexus18;
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
- for (const key of ['genexus', 'genexus18']) {
840
- if (cfgObj.mcpServers[key]) {
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.genexus = {
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
- // Drop the legacy `genexus18` key written by older build-from-source installs.
863
- if (cfgObj.servers.genexus18) delete cfgObj.servers.genexus18;
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
- for (const key of ['genexus', 'genexus18']) {
874
- if (cfgObj.servers[key]) {
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
- servers.genexus = {
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 (servers.genexus18) delete servers.genexus18;
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 (mcp.genexus) delete mcp.genexus;
920
- if (mcp.genexus18) delete mcp.genexus18;
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
- for (const key of ['genexus', 'genexus18']) {
932
- if (cfgObj.mcp[key]) {
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 ['genexus', 'genexus18']) {
939
- if (cfgObj.mcp.servers[key]) {
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 stripped = stripCodexGenexusBlocks(existing);
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('[mcp_servers.genexus]');
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('[mcp_servers.genexus.env]');
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 stripped = stripCodexGenexusBlocks(existing);
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
- // Walk line-by-line so values like `args = []` don't confuse the parser.
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
- const entry = parsed.mcpServers && parsed.mcpServers.genexus;
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 { command: entry.command || null, args: Array.isArray(entry.args) ? entry.args : [] };
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
- const entry = parsed.mcp?.servers?.genexus || parsed.mcp?.genexus;
1047
- if (!entry || !Array.isArray(entry.command) || entry.command.length === 0) return null;
1048
- return { command: entry.command[0], args: entry.command.slice(1) };
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
- const entry = parsed.servers && parsed.servers.genexus;
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 { command: entry.command || null, args: Array.isArray(entry.args) ? entry.args : [] };
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
- // Minimal extraction: find [mcp_servers.genexus] block and pull command = "..."
1060
- const blockRe = /\[mcp_servers\.genexus\]([\s\S]*?)(?=\n\[|$)/;
1061
- const m = raw.match(blockRe);
1062
- if (!m) return null;
1063
- const cmdMatch = m[1].match(/^\s*command\s*=\s*"((?:[^"\\]|\\.)*)"/m);
1064
- if (!cmdMatch) return null;
1065
- const command = cmdMatch[1].replace(/\\\\/g, '\\').replace(/\\"/g, '"');
1066
- return { command, args: [] };
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.genexus, 'genexus entry should be written');
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.genexus, 'genexus server entry written');
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.genexus, 'direct OpenCode entry should be written');
810
- assert.equal(written.mcp.genexus.enabled, true);
811
- assert.equal(written.mcp.genexus.disabled, undefined);
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.genexus.environment, { GX_CONFIG_PATH: cfgPath });
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.genexus, 'nested OpenCode entry should be written');
841
- assert.equal(written.mcp.servers.genexus.disabled, false);
842
- assert.equal(written.mcp.servers.genexus.enabled, undefined);
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.genexus, undefined);
845
- assert.deepEqual(written.mcp.servers.genexus.environment, { GX_CONFIG_PATH: cfgPath });
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.genexus : written.mcp.genexus;
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.genexus, 'new genexus entry present');
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: { genexus: { command: missing, args: [] } } }, null, 2));
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: { genexus: { command: 'npx' }, genexus18: { command: 'old' } } }, null, 2));
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.genexus, undefined, 'genexus removed');
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.48.0",
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.48.0": {
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.48.0": {
69
+ "GxMcp.Gateway/2.49.1": {
70
70
  "type": "project",
71
71
  "serviceable": false,
72
72
  "sha512": ""
Binary file
Binary file
@@ -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
- "Level": "Debug",
7
- "Path": "logs"
10
+ "Path": "logs",
11
+ "Level": "Debug"
8
12
  },
9
13
  "Server": {
10
- "McpStdio": true,
11
- "HttpPort": 5000
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