genexus-mcp 2.8.2 → 2.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -6
- package/cli/commands/axi.js +144 -6
- package/cli/index.js +13 -1
- package/cli/lib/config.js +395 -19
- package/cli/run.test.js +222 -0
- package/docs/llm_cli_mcp_playbook.md +1 -0
- 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/GxMcp.Gateway.pdb +0 -0
- package/publish/worker/GxMcp.Worker.exe +0 -0
- package/publish/worker/GxMcp.Worker.pdb +0 -0
package/README.md
CHANGED
|
@@ -175,13 +175,22 @@ Auto-detected and auto-configured by the installer:
|
|
|
175
175
|
| Claude Desktop | ✅ | Restart required after install |
|
|
176
176
|
| Claude Code (CLI) | ✅ | Reload session |
|
|
177
177
|
| Cursor | ✅ | Restart required |
|
|
178
|
-
| Antigravity | ✅ | Restart required |
|
|
178
|
+
| Antigravity | ✅ | Restart required; detected even before its MCP config exists |
|
|
179
|
+
| Gemini CLI | ✅ | — |
|
|
180
|
+
| OpenCode (CLI) | ✅ | Reads `opencode.json` / `opencode.jsonc` |
|
|
181
|
+
| Codex CLI | ✅ | Writes `~/.codex/config.toml` |
|
|
182
|
+
| VS Code / VS Code Insiders | ✅ | Native MCP (`User/mcp.json`); restart required |
|
|
183
|
+
| OpenCode Desktop | Detect-only | Reported as installed; add the server from the app's settings |
|
|
179
184
|
| Any MCP client | Manual | Use the JSON snippet printed by `init` |
|
|
180
185
|
|
|
186
|
+
Run **`npx genexus-mcp clients`** at any time to see which agents are installed, which have `genexus` registered, and whether any point at a stale gateway exe. To (re)register specific ones: `npx genexus-mcp clients add --clients antigravity,vscode`.
|
|
187
|
+
|
|
181
188
|
---
|
|
182
189
|
|
|
183
190
|
## Troubleshooting
|
|
184
191
|
|
|
192
|
+
First stop for any "the agent doesn't see GeneXus" problem: **`npx genexus-mcp clients`** (is it registered? does it point at a gateway exe that still exists?) and **`npx genexus-mcp doctor --mcp-smoke`**.
|
|
193
|
+
|
|
185
194
|
Most install issues fall into a handful of buckets — see **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** for fixes:
|
|
186
195
|
|
|
187
196
|
- Installer can't find GeneXus or the KB
|
|
@@ -375,12 +384,17 @@ This repo ships a set of **agent skills** under `.gemini/skills/` that any MCP-c
|
|
|
375
384
|
|
|
376
385
|
Third-party skills are Apache 2.0 (see [`.gemini/skills/NOTICE.md`](.gemini/skills/NOTICE.md)). To refresh against upstream, follow the steps in `NOTICE.md`.
|
|
377
386
|
|
|
378
|
-
### Nexus-IDE (VS Code extension)
|
|
387
|
+
### Nexus-IDE (VS Code extension — optional, not auto-installed)
|
|
388
|
+
|
|
389
|
+
`src/nexus-ide` is a lightweight, experimental VS Code extension in the repo. The installer **no longer packages or installs it** — VS Code is wired up as a native MCP client instead (see [Supported AI clients](#supported-ai-clients)). If you want the extension, build and install it manually:
|
|
390
|
+
|
|
391
|
+
```powershell
|
|
392
|
+
cd src/nexus-ide; npm ci; npm run compile
|
|
393
|
+
npx --yes @vscode/vsce package --out nexus-ide.vsix
|
|
394
|
+
code --install-extension nexus-ide.vsix --force
|
|
395
|
+
```
|
|
379
396
|
|
|
380
|
-
|
|
381
|
-
- Virtual filesystem using the `genexus://` scheme
|
|
382
|
-
- Dynamic KB explorer with multi-part editing (Source, Rules, Events, Variables)
|
|
383
|
-
- Built-in MCP discovery commands (tools, resources, prompts)
|
|
397
|
+
It provides a virtual filesystem (`genexus://` scheme), a KB explorer with multi-part editing, and MCP discovery commands.
|
|
384
398
|
|
|
385
399
|
### Automated release
|
|
386
400
|
|
package/cli/commands/axi.js
CHANGED
|
@@ -12,6 +12,8 @@ const {
|
|
|
12
12
|
patchClientConfig,
|
|
13
13
|
unpatchClientConfig,
|
|
14
14
|
getClientConfigTargets,
|
|
15
|
+
detectClientInstalled,
|
|
16
|
+
clientsStatus,
|
|
15
17
|
filterClientTargets,
|
|
16
18
|
listSupportedClientIds,
|
|
17
19
|
getLocalAppDataCacheDir,
|
|
@@ -701,6 +703,25 @@ async function handleDoctor(options, ctx) {
|
|
|
701
703
|
const inProcessLoad = buildInProcessBuildAssemblyLoadCheck(gxPath);
|
|
702
704
|
checks.push({ id: 'in_process_build_assembly_load', status: inProcessLoad.status, detail: inProcessLoad.detail });
|
|
703
705
|
|
|
706
|
+
// Client registration summary — one line answering "are my AI agents wired up?".
|
|
707
|
+
const clientRows = clientsStatus();
|
|
708
|
+
const installedRows = clientRows.filter((r) => r.installed);
|
|
709
|
+
const staleRows = clientRows.filter((r) => r.commandStale);
|
|
710
|
+
const installedUnregistered = installedRows.filter((r) => !r.registered && r.writeSupported);
|
|
711
|
+
let clientsStatusLevel = 'pass';
|
|
712
|
+
let clientsDetail;
|
|
713
|
+
if (staleRows.length > 0) {
|
|
714
|
+
clientsStatusLevel = 'warn';
|
|
715
|
+
clientsDetail = `${staleRows.map((r) => r.name).join(', ')} point at a missing gateway exe. Re-register: genexus-mcp clients add --clients ${staleRows.map((r) => r.id).join(',')}.`;
|
|
716
|
+
} else if (installedUnregistered.length > 0) {
|
|
717
|
+
clientsStatusLevel = 'warn';
|
|
718
|
+
clientsDetail = `${installedUnregistered.length} installed agent(s) not registered (${installedUnregistered.map((r) => r.name).join(', ')}). Run: genexus-mcp clients add --clients ${installedUnregistered.map((r) => r.id).join(',')}.`;
|
|
719
|
+
} else {
|
|
720
|
+
const reg = clientRows.filter((r) => r.registered).length;
|
|
721
|
+
clientsDetail = `${reg} agent(s) registered; ${installedRows.length} installed. Run \`genexus-mcp clients\` for the full table.`;
|
|
722
|
+
}
|
|
723
|
+
checks.push({ id: 'clients_registered', status: clientsStatusLevel, detail: clientsDetail });
|
|
724
|
+
|
|
704
725
|
if (data.gatewayExeFound) {
|
|
705
726
|
const probe = await probeGatewaySpawn();
|
|
706
727
|
checks.push({ id: 'gateway_spawn_probe', status: probe.status, detail: probe.detail });
|
|
@@ -1191,10 +1212,16 @@ async function runInteractiveInit(ctx) {
|
|
|
1191
1212
|
ctx.stderr.write('\n3) Select AI agents to register (y/N per agent; Enter accepts default):\n');
|
|
1192
1213
|
const selectedIds = [];
|
|
1193
1214
|
for (const target of platformTargets) {
|
|
1194
|
-
const
|
|
1195
|
-
const defaultYes = installed;
|
|
1196
|
-
const tag = installed ? 'detected' : 'not detected';
|
|
1197
|
-
|
|
1215
|
+
const detection = detectClientInstalled(target);
|
|
1216
|
+
const defaultYes = detection.installed;
|
|
1217
|
+
const tag = detection.installed ? 'detected' : 'not detected';
|
|
1218
|
+
// When not detected, show where we looked so the user understands why
|
|
1219
|
+
// (and can still type `y` to register a freshly-installed agent).
|
|
1220
|
+
let hint = '';
|
|
1221
|
+
if (!detection.installed && detection.markersChecked.length) {
|
|
1222
|
+
hint = ` — looked in ${detection.markersChecked[0]}`;
|
|
1223
|
+
}
|
|
1224
|
+
const prompt = ` - ${target.name} [${tag}${hint}] (${defaultYes ? 'Y/n' : 'y/N'}): `;
|
|
1198
1225
|
const ans = (await question(prompt)).trim().toLowerCase();
|
|
1199
1226
|
const yes = ans === '' ? defaultYes : (ans === 'y' || ans === 'yes');
|
|
1200
1227
|
if (yes) selectedIds.push(target.id);
|
|
@@ -1715,6 +1742,107 @@ async function handleUninstall(options, ctx) {
|
|
|
1715
1742
|
};
|
|
1716
1743
|
}
|
|
1717
1744
|
|
|
1745
|
+
async function handleClients(subcommand, options, ctx) {
|
|
1746
|
+
const sub = subcommand || 'list';
|
|
1747
|
+
|
|
1748
|
+
if (sub === 'list') {
|
|
1749
|
+
const rows = clientsStatus();
|
|
1750
|
+
const installedCount = rows.filter((r) => r.installed).length;
|
|
1751
|
+
const registeredCount = rows.filter((r) => r.registered).length;
|
|
1752
|
+
const help = [];
|
|
1753
|
+
const installedUnregistered = rows.filter((r) => r.installed && !r.registered && r.writeSupported);
|
|
1754
|
+
if (installedUnregistered.length > 0) {
|
|
1755
|
+
help.push(`Register installed-but-unregistered agents: genexus-mcp clients add --clients ${installedUnregistered.map((r) => r.id).join(',')}`);
|
|
1756
|
+
}
|
|
1757
|
+
const stale = rows.filter((r) => r.commandStale);
|
|
1758
|
+
if (stale.length > 0) {
|
|
1759
|
+
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(',')}`);
|
|
1760
|
+
}
|
|
1761
|
+
for (const r of rows) {
|
|
1762
|
+
if (r.installed && !r.writeSupported && r.note) help.push(`${r.name}: ${r.note}`);
|
|
1763
|
+
}
|
|
1764
|
+
return {
|
|
1765
|
+
exitCode: ctx.EXIT_CODES.OK,
|
|
1766
|
+
envelope: {
|
|
1767
|
+
ok: {
|
|
1768
|
+
clients: rows,
|
|
1769
|
+
summary: { total: rows.length, installed: installedCount, registered: registeredCount }
|
|
1770
|
+
},
|
|
1771
|
+
help
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
if (sub === 'add' || sub === 'remove') {
|
|
1777
|
+
const ids = resolveClientIds(options);
|
|
1778
|
+
if (sub === 'add' && (!ids || ids.length === 0)) {
|
|
1779
|
+
return {
|
|
1780
|
+
exitCode: ctx.EXIT_CODES.USAGE,
|
|
1781
|
+
envelope: usageEnvelope('`clients add` requires --clients <csv> (e.g. --clients antigravity,vscode).', ctx.EXIT_CODES.USAGE)
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
const validation = validateClientIds(ids);
|
|
1785
|
+
if (!validation.ok) {
|
|
1786
|
+
return { exitCode: ctx.EXIT_CODES.USAGE, envelope: usageEnvelope(validation.message, ctx.EXIT_CODES.USAGE) };
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
if (sub === 'add') {
|
|
1790
|
+
const configPath = resolveConfigPathNoMutate(ctx.cwd);
|
|
1791
|
+
if (!configPath) {
|
|
1792
|
+
return {
|
|
1793
|
+
exitCode: ctx.EXIT_CODES.ERROR,
|
|
1794
|
+
envelope: operationalErrorEnvelope(
|
|
1795
|
+
'No config.json found to point the clients at. Run `genexus-mcp init` first (or run from a KB folder).',
|
|
1796
|
+
ctx.EXIT_CODES.ERROR
|
|
1797
|
+
)
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
let patch;
|
|
1801
|
+
try {
|
|
1802
|
+
// Explicit add: write even if install markers are absent (the user asked for it).
|
|
1803
|
+
patch = patchClientConfig(configPath, { ids, onlyExisting: false });
|
|
1804
|
+
} catch (err) {
|
|
1805
|
+
return {
|
|
1806
|
+
exitCode: ctx.EXIT_CODES.ERROR,
|
|
1807
|
+
envelope: operationalErrorEnvelope(
|
|
1808
|
+
sanitizeOperationalMessage(`Client registration failed: ${err && err.message ? err.message : 'unknown error'}`),
|
|
1809
|
+
ctx.EXIT_CODES.ERROR
|
|
1810
|
+
)
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
const help = [];
|
|
1814
|
+
if (patch.patched.length > 0) help.push('Restart the affected AI client(s) to load the new MCP config.');
|
|
1815
|
+
if (patch.failed.length > 0) help.push('Some clients failed (see meta.failedClients).');
|
|
1816
|
+
return {
|
|
1817
|
+
exitCode: ctx.EXIT_CODES.OK,
|
|
1818
|
+
envelope: {
|
|
1819
|
+
ok: { action: 'clients.add', configPath, patchedClients: patch.patched, patchedCount: patch.patched.length },
|
|
1820
|
+
help,
|
|
1821
|
+
meta: { failedClients: patch.failed, skippedClients: patch.skipped }
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// remove
|
|
1827
|
+
const unpatch = unpatchClientConfig(ids ? { ids } : {});
|
|
1828
|
+
const help = [];
|
|
1829
|
+
if (unpatch.removed.length > 0) help.push('Restart the affected AI client(s) to drop the stale MCP connection.');
|
|
1830
|
+
return {
|
|
1831
|
+
exitCode: ctx.EXIT_CODES.OK,
|
|
1832
|
+
envelope: {
|
|
1833
|
+
ok: { action: 'clients.remove', removedClients: unpatch.removed, removedCount: unpatch.removed.length },
|
|
1834
|
+
help,
|
|
1835
|
+
meta: { skippedClients: unpatch.skipped, failedClients: unpatch.failed }
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
return {
|
|
1841
|
+
exitCode: ctx.EXIT_CODES.USAGE,
|
|
1842
|
+
envelope: usageEnvelope('clients supports subcommands `list`, `add`, `remove`.', ctx.EXIT_CODES.USAGE)
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1718
1846
|
async function handleKb(subcommand, options, ctx) {
|
|
1719
1847
|
const data = buildStatusData(ctx.cwd);
|
|
1720
1848
|
if (!data.configPath) {
|
|
@@ -1900,6 +2028,15 @@ function commandHelpMap() {
|
|
|
1900
2028
|
'genexus-mcp kb remove --name sales'
|
|
1901
2029
|
]
|
|
1902
2030
|
},
|
|
2031
|
+
clients: {
|
|
2032
|
+
usage: 'genexus-mcp clients [list] [--format ...] OR genexus-mcp clients add --clients <csv> OR genexus-mcp clients remove [--clients <csv>]',
|
|
2033
|
+
examples: [
|
|
2034
|
+
'genexus-mcp clients # show every AI agent: installed? registered? where?',
|
|
2035
|
+
'genexus-mcp clients --format json',
|
|
2036
|
+
'genexus-mcp clients add --clients antigravity,vscode',
|
|
2037
|
+
'genexus-mcp clients remove --clients cursor'
|
|
2038
|
+
]
|
|
2039
|
+
},
|
|
1903
2040
|
llm: {
|
|
1904
2041
|
usage: 'genexus-mcp llm help [--full] [--fields f1,f2] [--format toon|json|text]',
|
|
1905
2042
|
examples: ['genexus-mcp llm help --format json', 'genexus-mcp llm help --full --format json']
|
|
@@ -1935,8 +2072,8 @@ async function handleHome(_options, ctx) {
|
|
|
1935
2072
|
description: 'GeneXus MCP launcher and AXI-oriented utility CLI',
|
|
1936
2073
|
ready: data.ready,
|
|
1937
2074
|
next: data.ready
|
|
1938
|
-
? ['genexus-mcp status', 'genexus-mcp doctor --mcp-smoke', 'genexus-mcp tools list --limit 10', 'genexus-mcp layout status'
|
|
1939
|
-
: ['genexus-mcp status', 'genexus-mcp doctor --full', 'genexus-mcp init --kb "<kbPath>" --gx "<geneXusPath>"']
|
|
2075
|
+
? ['genexus-mcp status', 'genexus-mcp clients', 'genexus-mcp doctor --mcp-smoke', 'genexus-mcp tools list --limit 10', 'genexus-mcp layout status']
|
|
2076
|
+
: ['genexus-mcp status', 'genexus-mcp clients', 'genexus-mcp doctor --full', 'genexus-mcp init --kb "<kbPath>" --gx "<geneXusPath>"']
|
|
1940
2077
|
},
|
|
1941
2078
|
help: []
|
|
1942
2079
|
}
|
|
@@ -2078,6 +2215,7 @@ module.exports = {
|
|
|
2078
2215
|
handleWhoami,
|
|
2079
2216
|
handleUninstall,
|
|
2080
2217
|
handleKb,
|
|
2218
|
+
handleClients,
|
|
2081
2219
|
handleHome,
|
|
2082
2220
|
handleLlmHelp,
|
|
2083
2221
|
handleLayout,
|
package/cli/index.js
CHANGED
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
handleWhoami,
|
|
20
20
|
handleUninstall,
|
|
21
21
|
handleKb,
|
|
22
|
+
handleClients,
|
|
22
23
|
handleHome,
|
|
23
24
|
handleLlmHelp,
|
|
24
25
|
handleLayout,
|
|
@@ -55,7 +56,7 @@ const GLOBAL_DEFAULTS = {
|
|
|
55
56
|
help: false
|
|
56
57
|
};
|
|
57
58
|
|
|
58
|
-
const KNOWN_COMMANDS = new Set(['status', 'doctor', 'tools', 'config', 'init', 'setup', 'whoami', 'uninstall', 'kb', 'help', 'home', 'axi', 'llm', 'layout', 'update']);
|
|
59
|
+
const KNOWN_COMMANDS = new Set(['status', 'doctor', 'tools', 'config', 'init', 'setup', 'whoami', 'uninstall', 'kb', 'clients', 'help', 'home', 'axi', 'llm', 'layout', 'update']);
|
|
59
60
|
|
|
60
61
|
function parseArgs(argv) {
|
|
61
62
|
const result = {
|
|
@@ -115,6 +116,11 @@ function parseArgs(argv) {
|
|
|
115
116
|
tokens.shift();
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
if (result.command === 'clients' && ['list', 'add', 'remove'].includes(tokens[0])) {
|
|
120
|
+
result.subcommand = tokens[0];
|
|
121
|
+
tokens.shift();
|
|
122
|
+
}
|
|
123
|
+
|
|
118
124
|
if (result.command === 'layout' && (tokens[0] === 'status' || tokens[0] === 'run' || tokens[0] === 'inspect')) {
|
|
119
125
|
result.subcommand = tokens[0];
|
|
120
126
|
tokens.shift();
|
|
@@ -395,6 +401,9 @@ function resolveMetaCommand(parsed, targetHelp) {
|
|
|
395
401
|
if (parsed.command === 'kb') {
|
|
396
402
|
return parsed.subcommand ? `kb.${parsed.subcommand}` : 'kb';
|
|
397
403
|
}
|
|
404
|
+
if (parsed.command === 'clients') {
|
|
405
|
+
return parsed.subcommand ? `clients.${parsed.subcommand}` : 'clients.list';
|
|
406
|
+
}
|
|
398
407
|
if (parsed.command === 'update') return 'update';
|
|
399
408
|
return parsed.command || 'unknown';
|
|
400
409
|
}
|
|
@@ -523,6 +532,9 @@ async function main(argv) {
|
|
|
523
532
|
}
|
|
524
533
|
result = await handleKb(parsed.subcommand, parsed.options, ctx);
|
|
525
534
|
break;
|
|
535
|
+
case 'clients':
|
|
536
|
+
result = await handleClients(parsed.subcommand, parsed.options, ctx);
|
|
537
|
+
break;
|
|
526
538
|
case 'update':
|
|
527
539
|
result = await handleUpdate(parsed.options, ctx);
|
|
528
540
|
break;
|
package/cli/lib/config.js
CHANGED
|
@@ -237,16 +237,132 @@ function directoryLooksLikeKnowledgeBase(dir) {
|
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
// Strip // and /* */ comments and trailing commas while respecting string
|
|
241
|
+
// literals, so we can parse JSONC configs (VS Code's mcp.json/settings.json and
|
|
242
|
+
// OpenCode's opencode.jsonc are JSONC). Comments are NOT preserved on rewrite.
|
|
243
|
+
//
|
|
244
|
+
// Trailing-comma removal is done INSIDE the scanner (a comma is deferred and only
|
|
245
|
+
// emitted once we know the next significant char isn't a closing brace/bracket),
|
|
246
|
+
// not by a post-hoc regex — a regex over the whole text would also strip commas
|
|
247
|
+
// that live inside string values (e.g. "see foo, ]" -> "see foo ]"). Only `"`
|
|
248
|
+
// opens a string: JSON/JSONC has no single-quoted strings.
|
|
249
|
+
function stripJsonComments(text) {
|
|
250
|
+
let out = '';
|
|
251
|
+
let inString = false;
|
|
252
|
+
let inLine = false;
|
|
253
|
+
let inBlock = false;
|
|
254
|
+
let pendingComma = false;
|
|
255
|
+
// Resolve a deferred comma: keep it unless the next significant char closes a
|
|
256
|
+
// container (then it was a trailing comma and gets dropped).
|
|
257
|
+
const flushComma = (nextSignificant) => {
|
|
258
|
+
if (pendingComma) {
|
|
259
|
+
if (nextSignificant !== '}' && nextSignificant !== ']') out += ',';
|
|
260
|
+
pendingComma = false;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
264
|
+
const ch = text[i];
|
|
265
|
+
const next = text[i + 1];
|
|
266
|
+
if (inLine) {
|
|
267
|
+
if (ch === '\n') inLine = false;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (inBlock) {
|
|
271
|
+
if (ch === '*' && next === '/') { inBlock = false; i += 1; }
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (inString) {
|
|
275
|
+
out += ch;
|
|
276
|
+
if (ch === '\\') { out += next; i += 1; continue; }
|
|
277
|
+
if (ch === '"') inString = false;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
// Outside any string/comment.
|
|
281
|
+
if (ch === '/' && next === '/') { inLine = true; i += 1; continue; }
|
|
282
|
+
if (ch === '/' && next === '*') { inBlock = true; i += 1; continue; }
|
|
283
|
+
if (ch === ' ' || ch === '\t' || ch === '\r' || ch === '\n') {
|
|
284
|
+
// Whitespace between a deferred comma and the next token is collapsed
|
|
285
|
+
// (JSON.parse ignores it); otherwise emit it verbatim.
|
|
286
|
+
if (!pendingComma) out += ch;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (ch === ',') {
|
|
290
|
+
flushComma(','); // a prior comma followed by another comma is kept as-is
|
|
291
|
+
pendingComma = true; // defer this one until we see what follows
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
flushComma(ch);
|
|
295
|
+
out += ch;
|
|
296
|
+
if (ch === '"') inString = true;
|
|
297
|
+
}
|
|
298
|
+
flushComma('');
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
|
|
240
302
|
function readJsonFileSafe(filePath) {
|
|
241
303
|
try {
|
|
242
304
|
const raw = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
|
|
243
305
|
if (!raw.trim()) return {};
|
|
244
|
-
|
|
306
|
+
try {
|
|
307
|
+
return JSON.parse(raw);
|
|
308
|
+
} catch {
|
|
309
|
+
// Fall back to a JSONC-tolerant parse before giving up, so a commented
|
|
310
|
+
// VS Code / OpenCode config isn't treated as corrupt.
|
|
311
|
+
return JSON.parse(stripJsonComments(raw));
|
|
312
|
+
}
|
|
245
313
|
} catch {
|
|
246
314
|
return null;
|
|
247
315
|
}
|
|
248
316
|
}
|
|
249
317
|
|
|
318
|
+
// Atomic write: stage to a temp file then rename over the target, so a crash
|
|
319
|
+
// mid-write can never leave a client's config truncated.
|
|
320
|
+
function writeFileAtomic(filePath, content) {
|
|
321
|
+
const tmp = `${filePath}.tmp-${process.pid}`;
|
|
322
|
+
fs.writeFileSync(tmp, content);
|
|
323
|
+
try {
|
|
324
|
+
fs.renameSync(tmp, filePath);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
try { fs.rmSync(tmp, { force: true }); } catch { /* ignore */ }
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Back up a client config once per process run before the first mutation, so the
|
|
332
|
+
// user has a restore point (the old build-from-source install.ps1 did this; the
|
|
333
|
+
// CLI now owns it). Best-effort \u2014 a failed backup never blocks the write.
|
|
334
|
+
const _backedUpThisRun = new Set();
|
|
335
|
+
function backupClientConfigOnce(filePath) {
|
|
336
|
+
if (!fs.existsSync(filePath)) return null;
|
|
337
|
+
// Case-fold the dedupe key only on Windows; lowercasing on a case-sensitive
|
|
338
|
+
// filesystem could merge two genuinely distinct paths.
|
|
339
|
+
const resolved = path.resolve(filePath);
|
|
340
|
+
const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
341
|
+
if (_backedUpThisRun.has(key)) return null;
|
|
342
|
+
try {
|
|
343
|
+
const d = new Date();
|
|
344
|
+
const stamp = d.toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
345
|
+
const bak = `${filePath}.${stamp}.bak`;
|
|
346
|
+
fs.copyFileSync(filePath, bak);
|
|
347
|
+
_backedUpThisRun.add(key);
|
|
348
|
+
return bak;
|
|
349
|
+
} catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Write JSON to a client config: back up, serialize, write atomically.
|
|
355
|
+
function writeClientJson(filePath, obj) {
|
|
356
|
+
backupClientConfigOnce(filePath);
|
|
357
|
+
writeFileAtomic(filePath, JSON.stringify(obj, null, 2));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Write raw text to a client config (e.g. Codex TOML): back up + write atomically.
|
|
361
|
+
function writeClientText(filePath, content) {
|
|
362
|
+
backupClientConfigOnce(filePath);
|
|
363
|
+
writeFileAtomic(filePath, content);
|
|
364
|
+
}
|
|
365
|
+
|
|
250
366
|
function resolveConfigPathNoMutate(cwd) {
|
|
251
367
|
const cwdConfigPath = path.join(cwd, 'config.json');
|
|
252
368
|
if (process.env.GX_CONFIG_PATH && fs.existsSync(process.env.GX_CONFIG_PATH)) {
|
|
@@ -298,67 +414,241 @@ function getLauncher() {
|
|
|
298
414
|
: { command: process.platform === 'win32' ? 'npx.cmd' : 'npx', args: ['-y', 'genexus-mcp@latest'] };
|
|
299
415
|
}
|
|
300
416
|
|
|
417
|
+
// Antigravity (Google's agentic IDE) ships its MCP config under ~/.gemini.
|
|
418
|
+
// The newer unified location (shared across Antigravity CLI / IDE / SDK) is
|
|
419
|
+
// ~/.gemini/config/mcp_config.json; the older IDE-specific one is
|
|
420
|
+
// ~/.gemini/antigravity/mcp_config.json. We write to the unified path when its
|
|
421
|
+
// parent dir already exists, else fall back to the IDE-specific path.
|
|
422
|
+
function resolveAntigravityConfigPath(home) {
|
|
423
|
+
// Only target the unified location when its file already exists (the user has
|
|
424
|
+
// adopted it); otherwise write the IDE-specific path, which is the location
|
|
425
|
+
// Antigravity reliably reads and was confirmed working in the field.
|
|
426
|
+
const unified = path.join(home, '.gemini', 'config', 'mcp_config.json');
|
|
427
|
+
if (fs.existsSync(unified)) return unified;
|
|
428
|
+
return path.join(home, '.gemini', 'antigravity', 'mcp_config.json');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// OpenCode CLI accepts either opencode.jsonc or opencode.json. Prefer an
|
|
432
|
+
// existing .jsonc so we don't strand the user's commented config, else .json.
|
|
433
|
+
function resolveOpenCodeConfigPath(xdgConfig) {
|
|
434
|
+
const jsonc = path.join(xdgConfig, 'opencode', 'opencode.jsonc');
|
|
435
|
+
if (fs.existsSync(jsonc)) return jsonc;
|
|
436
|
+
return path.join(xdgConfig, 'opencode', 'opencode.json');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// VS Code stores its user profile (and native MCP mcp.json) in a per-platform
|
|
440
|
+
// location. `variant` is 'Code' (stable) or 'Code - Insiders'.
|
|
441
|
+
function vscodeUserDir(variant, { appData, macAppSupport, xdgConfig }) {
|
|
442
|
+
if (process.platform === 'win32') return path.join(appData, variant, 'User');
|
|
443
|
+
if (process.platform === 'darwin') return path.join(macAppSupport, variant, 'User');
|
|
444
|
+
return path.join(xdgConfig, variant, 'User');
|
|
445
|
+
}
|
|
446
|
+
|
|
301
447
|
function getClientConfigTargets() {
|
|
302
448
|
const home = os.homedir();
|
|
303
449
|
const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
450
|
+
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
451
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
452
|
+
const macAppSupport = path.join(home, 'Library', 'Application Support');
|
|
453
|
+
const vscodeStableUser = vscodeUserDir('Code', { appData, macAppSupport, xdgConfig });
|
|
454
|
+
const vscodeInsidersUser = vscodeUserDir('Code - Insiders', { appData, macAppSupport, xdgConfig });
|
|
455
|
+
|
|
456
|
+
// `installMarkers` prove the AGENT is installed, independent of whether our
|
|
457
|
+
// MCP config file exists yet. This is the fix for the field report where the
|
|
458
|
+
// wizard showed Antigravity as "not detected": Antigravity does not create
|
|
459
|
+
// ~/.gemini/antigravity/mcp_config.json until the user adds an MCP server, so
|
|
460
|
+
// detecting by config-file presence alone was chicken-and-egg.
|
|
304
461
|
return [
|
|
305
462
|
{
|
|
306
463
|
id: 'claude-desktop-win',
|
|
307
464
|
name: 'Claude Desktop (Windows)',
|
|
308
465
|
format: 'mcpServers',
|
|
309
466
|
path: path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json'),
|
|
310
|
-
platforms: ['win32']
|
|
467
|
+
platforms: ['win32'],
|
|
468
|
+
installMarkers: [
|
|
469
|
+
path.join(localAppData, 'AnthropicClaude'),
|
|
470
|
+
path.join(appData, 'Claude')
|
|
471
|
+
]
|
|
311
472
|
},
|
|
312
473
|
{
|
|
313
474
|
id: 'claude-desktop-mac',
|
|
314
475
|
name: 'Claude Desktop (macOS)',
|
|
315
476
|
format: 'mcpServers',
|
|
316
477
|
path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
|
|
317
|
-
platforms: ['darwin']
|
|
478
|
+
platforms: ['darwin'],
|
|
479
|
+
installMarkers: [
|
|
480
|
+
path.join(macAppSupport, 'Claude'),
|
|
481
|
+
'/Applications/Claude.app'
|
|
482
|
+
]
|
|
318
483
|
},
|
|
319
484
|
{
|
|
320
485
|
id: 'antigravity',
|
|
321
486
|
name: 'Antigravity',
|
|
322
487
|
format: 'mcpServers',
|
|
323
|
-
path:
|
|
488
|
+
path: resolveAntigravityConfigPath(home),
|
|
489
|
+
// Unambiguous Antigravity markers only. ~/.gemini/config is NOT a
|
|
490
|
+
// marker — gemini-cli can create ~/.gemini, and we'd false-positive;
|
|
491
|
+
// it's still used as the write path (resolveAntigravityConfigPath)
|
|
492
|
+
// once a real Antigravity install is confirmed by these markers.
|
|
493
|
+
installMarkers: [
|
|
494
|
+
path.join(localAppData, 'Programs', 'Antigravity'),
|
|
495
|
+
path.join(appData, 'Antigravity'),
|
|
496
|
+
path.join(home, '.antigravity'),
|
|
497
|
+
path.join(home, '.gemini', 'antigravity')
|
|
498
|
+
]
|
|
324
499
|
},
|
|
325
500
|
{
|
|
326
501
|
id: 'claude-code',
|
|
327
502
|
name: 'Claude Code',
|
|
328
503
|
format: 'mcpServers',
|
|
329
|
-
path: path.join(home, '.claude.json')
|
|
504
|
+
path: path.join(home, '.claude.json'),
|
|
505
|
+
installMarkers: [
|
|
506
|
+
path.join(home, '.claude.json'),
|
|
507
|
+
path.join(home, '.claude')
|
|
508
|
+
]
|
|
330
509
|
},
|
|
331
510
|
{
|
|
332
511
|
id: 'gemini-cli',
|
|
333
512
|
name: 'Gemini CLI',
|
|
334
513
|
format: 'mcpServers',
|
|
335
|
-
path: path.join(home, '.gemini', 'settings.json')
|
|
514
|
+
path: path.join(home, '.gemini', 'settings.json'),
|
|
515
|
+
installMarkers: [
|
|
516
|
+
path.join(home, '.gemini', 'settings.json')
|
|
517
|
+
]
|
|
336
518
|
},
|
|
337
519
|
{
|
|
338
520
|
id: 'cursor',
|
|
339
521
|
name: 'Cursor',
|
|
340
522
|
format: 'mcpServers',
|
|
341
|
-
path: path.join(home, '.cursor', 'mcp.json')
|
|
523
|
+
path: path.join(home, '.cursor', 'mcp.json'),
|
|
524
|
+
installMarkers: [
|
|
525
|
+
path.join(home, '.cursor'),
|
|
526
|
+
path.join(localAppData, 'Programs', 'cursor'),
|
|
527
|
+
'/Applications/Cursor.app'
|
|
528
|
+
]
|
|
342
529
|
},
|
|
343
530
|
{
|
|
344
531
|
id: 'opencode',
|
|
345
|
-
name: 'OpenCode',
|
|
532
|
+
name: 'OpenCode (CLI)',
|
|
346
533
|
format: 'opencode',
|
|
347
|
-
path:
|
|
534
|
+
path: resolveOpenCodeConfigPath(xdgConfig),
|
|
535
|
+
installMarkers: [
|
|
536
|
+
path.join(xdgConfig, 'opencode'),
|
|
537
|
+
path.join(home, '.local', 'share', 'opencode')
|
|
538
|
+
]
|
|
348
539
|
},
|
|
349
540
|
{
|
|
350
541
|
id: 'codex-cli',
|
|
351
542
|
name: 'Codex CLI',
|
|
352
543
|
format: 'codex-toml',
|
|
353
|
-
path: path.join(home, '.codex', 'config.toml')
|
|
544
|
+
path: path.join(home, '.codex', 'config.toml'),
|
|
545
|
+
installMarkers: [
|
|
546
|
+
path.join(home, '.codex')
|
|
547
|
+
]
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
id: 'opencode-desktop',
|
|
551
|
+
name: 'OpenCode Desktop',
|
|
552
|
+
// Detect-only: the Desktop app's MCP config schema differs from the CLI
|
|
553
|
+
// and isn't auto-written yet. We report it so the user knows it's there
|
|
554
|
+
// and how to wire it up, but never mutate its config blindly.
|
|
555
|
+
format: 'manual',
|
|
556
|
+
writeSupported: false,
|
|
557
|
+
manualNote: 'OpenCode Desktop: add the genexus MCP server from the app\'s settings (automatic registration not supported yet).',
|
|
558
|
+
path: path.join(appData, 'ai.opencode.desktop', 'mcp.json'),
|
|
559
|
+
installMarkers: [
|
|
560
|
+
path.join(localAppData, 'Programs', '@opencode-aidesktop'),
|
|
561
|
+
path.join(appData, 'ai.opencode.desktop'),
|
|
562
|
+
'/Applications/OpenCode.app'
|
|
563
|
+
]
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
id: 'vscode',
|
|
567
|
+
name: 'VS Code',
|
|
568
|
+
format: 'vscode-servers',
|
|
569
|
+
path: path.join(vscodeStableUser, 'mcp.json'),
|
|
570
|
+
installMarkers: [vscodeStableUser]
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
id: 'vscode-insiders',
|
|
574
|
+
name: 'VS Code Insiders',
|
|
575
|
+
format: 'vscode-servers',
|
|
576
|
+
path: path.join(vscodeInsidersUser, 'mcp.json'),
|
|
577
|
+
installMarkers: [vscodeInsidersUser]
|
|
354
578
|
}
|
|
355
579
|
];
|
|
356
580
|
}
|
|
357
581
|
|
|
582
|
+
// Decide whether an agent is installed (independent of whether OUR config file
|
|
583
|
+
// exists). Returns the installed flag plus diagnostics so the wizard can show
|
|
584
|
+
// the user exactly where it looked when an agent is reported "not detected".
|
|
585
|
+
function detectClientInstalled(client) {
|
|
586
|
+
const markers = Array.isArray(client.installMarkers) ? client.installMarkers : [];
|
|
587
|
+
const hasConfig = fs.existsSync(client.path);
|
|
588
|
+
let markerHit = null;
|
|
589
|
+
for (const m of markers) {
|
|
590
|
+
if (fs.existsSync(m)) {
|
|
591
|
+
markerHit = m;
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
installed: hasConfig || markerHit !== null,
|
|
597
|
+
hasConfig,
|
|
598
|
+
markerHit,
|
|
599
|
+
markersChecked: markers
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
358
603
|
function listSupportedClientIds() {
|
|
359
604
|
return getClientConfigTargets().map((c) => c.id);
|
|
360
605
|
}
|
|
361
606
|
|
|
607
|
+
// Judge whether a registered launcher command is healthy. npx/node/genexus-mcp
|
|
608
|
+
// shims resolve at runtime so we can't fault them; any other launcher referenced
|
|
609
|
+
// by an explicit path (a separator in the command) that no longer exists on disk
|
|
610
|
+
// is the classic "Failed to connect / still on old version" cause after an
|
|
611
|
+
// install dir moved or was cleaned — covers .exe, .bat, .cmd, .sh, extensionless.
|
|
612
|
+
function clientCommandHealth(entry) {
|
|
613
|
+
if (!entry || !entry.command) return { stale: false, reason: null };
|
|
614
|
+
const cmd = String(entry.command);
|
|
615
|
+
if (/(^|[\\/])(npx|npx\.cmd|node|node\.exe|genexus-mcp|genexus-mcp\.cmd)$/i.test(cmd)) {
|
|
616
|
+
return { stale: false, reason: null };
|
|
617
|
+
}
|
|
618
|
+
if (/[\\/]/.test(cmd) && !fs.existsSync(cmd)) {
|
|
619
|
+
return { stale: true, reason: 'configured launcher does not exist on disk' };
|
|
620
|
+
}
|
|
621
|
+
return { stale: false, reason: null };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Read-only report of every supported agent on this platform: is it installed,
|
|
625
|
+
// is genexus registered, where, what launcher command it points at, and whether
|
|
626
|
+
// that command is stale. Backs the `genexus-mcp clients` command.
|
|
627
|
+
function clientsStatus(opts = {}) {
|
|
628
|
+
const targets = filterClientTargets(getClientConfigTargets(), {
|
|
629
|
+
ids: opts.ids,
|
|
630
|
+
platform: process.platform
|
|
631
|
+
});
|
|
632
|
+
return targets.map((client) => {
|
|
633
|
+
const det = detectClientInstalled(client);
|
|
634
|
+
const entry = readClientCommandEntry(client);
|
|
635
|
+
const health = clientCommandHealth(entry);
|
|
636
|
+
return {
|
|
637
|
+
id: client.id,
|
|
638
|
+
name: client.name,
|
|
639
|
+
installed: det.installed,
|
|
640
|
+
registered: entry !== null,
|
|
641
|
+
writeSupported: client.writeSupported !== false,
|
|
642
|
+
configPath: client.path,
|
|
643
|
+
command: entry && entry.command ? entry.command : null,
|
|
644
|
+
commandStale: health.stale,
|
|
645
|
+
commandStaleReason: health.reason,
|
|
646
|
+
detectedAt: det.markerHit || (det.hasConfig ? client.path : null),
|
|
647
|
+
note: client.writeSupported === false ? (client.manualNote || null) : null
|
|
648
|
+
};
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
362
652
|
function filterClientTargets(targets, opts = {}) {
|
|
363
653
|
const { ids, onlyExisting, platform } = opts;
|
|
364
654
|
let out = targets;
|
|
@@ -396,13 +686,29 @@ function patchClientConfig(targetConfigPath, opts = {}) {
|
|
|
396
686
|
const skipped = [];
|
|
397
687
|
|
|
398
688
|
for (const client of candidates) {
|
|
399
|
-
|
|
689
|
+
// Detect-only agents (e.g. OpenCode Desktop) can't be auto-written; surface
|
|
690
|
+
// the manual step instead of pretending we registered them.
|
|
691
|
+
if (client.writeSupported === false) {
|
|
692
|
+
if (detectClientInstalled(client).installed) {
|
|
693
|
+
skipped.push({ client: client.name, reason: client.manualNote || 'manual setup required' });
|
|
694
|
+
}
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
// "Installed" keys off install markers (the agent itself is present), not
|
|
698
|
+
// just our config file — otherwise agents that don't pre-create their MCP
|
|
699
|
+
// config (e.g. Antigravity) are wrongly skipped as "not installed".
|
|
700
|
+
if (onlyExisting && !detectClientInstalled(client).installed) {
|
|
400
701
|
skipped.push({ client: client.name, reason: 'not installed' });
|
|
401
702
|
continue;
|
|
402
703
|
}
|
|
403
704
|
try {
|
|
404
705
|
fs.mkdirSync(path.dirname(client.path), { recursive: true });
|
|
405
706
|
applyClientEntry(client, launcher, targetConfigPath);
|
|
707
|
+
// Read-back: confirm the entry is actually present and the file still
|
|
708
|
+
// parses, so a silently-corrupted write is reported as a failure.
|
|
709
|
+
if (!readClientCommandEntry(client)) {
|
|
710
|
+
throw new Error('post-write verification failed (genexus entry not found after write)');
|
|
711
|
+
}
|
|
406
712
|
patched.push(client.name);
|
|
407
713
|
} catch (err) {
|
|
408
714
|
failed.push({ client: client.name, reason: err && err.message ? err.message : 'Unknown error' });
|
|
@@ -423,6 +729,11 @@ function unpatchClientConfig(opts = {}) {
|
|
|
423
729
|
const failed = [];
|
|
424
730
|
|
|
425
731
|
for (const client of targets) {
|
|
732
|
+
// Detect-only agents were never written by us — nothing to remove.
|
|
733
|
+
if (client.writeSupported === false) {
|
|
734
|
+
skipped.push({ client: client.name, reason: 'manual setup (not managed by genexus-mcp)' });
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
426
737
|
try {
|
|
427
738
|
const wasRemoved = removeClientEntry(client);
|
|
428
739
|
if (wasRemoved) removed.push(client.name);
|
|
@@ -443,6 +754,8 @@ function applyClientEntry(client, launcher, targetConfigPath) {
|
|
|
443
754
|
return applyOpenCodeJson(client.path, launcher, targetConfigPath);
|
|
444
755
|
case 'codex-toml':
|
|
445
756
|
return applyCodexToml(client.path, launcher, targetConfigPath);
|
|
757
|
+
case 'vscode-servers':
|
|
758
|
+
return applyVsCodeServersJson(client.path, launcher, targetConfigPath);
|
|
446
759
|
default:
|
|
447
760
|
throw new Error(`Unknown client format: ${client.format}`);
|
|
448
761
|
}
|
|
@@ -456,6 +769,8 @@ function removeClientEntry(client) {
|
|
|
456
769
|
return removeOpenCodeJson(client.path);
|
|
457
770
|
case 'codex-toml':
|
|
458
771
|
return removeCodexToml(client.path);
|
|
772
|
+
case 'vscode-servers':
|
|
773
|
+
return removeVsCodeServersJson(client.path);
|
|
459
774
|
default:
|
|
460
775
|
throw new Error(`Unknown client format: ${client.format}`);
|
|
461
776
|
}
|
|
@@ -467,16 +782,63 @@ function applyMcpServersJson(filePath, launcher, targetConfigPath) {
|
|
|
467
782
|
const cfgObj = parsed || {};
|
|
468
783
|
cfgObj.mcpServers = cfgObj.mcpServers || {};
|
|
469
784
|
cfgObj.mcpServers.genexus = { ...launcher, env: { GX_CONFIG_PATH: targetConfigPath } };
|
|
470
|
-
|
|
785
|
+
// Drop the legacy `genexus18` key from older build-from-source installs so the
|
|
786
|
+
// user isn't left with two duplicate servers (and colliding tool names).
|
|
787
|
+
if (cfgObj.mcpServers.genexus18) delete cfgObj.mcpServers.genexus18;
|
|
788
|
+
writeClientJson(filePath, cfgObj);
|
|
471
789
|
}
|
|
472
790
|
|
|
473
791
|
function removeMcpServersJson(filePath) {
|
|
474
792
|
const parsed = readJsonFileSafe(filePath);
|
|
475
793
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
476
794
|
const cfgObj = parsed || {};
|
|
477
|
-
if (!cfgObj.mcpServers
|
|
478
|
-
|
|
479
|
-
|
|
795
|
+
if (!cfgObj.mcpServers) return false;
|
|
796
|
+
// Remove the current key plus the legacy `genexus18` key written by older
|
|
797
|
+
// versions of the build-from-source install.ps1, so uninstall fully cleans up
|
|
798
|
+
// regardless of which installer wrote the entry.
|
|
799
|
+
let removedAny = false;
|
|
800
|
+
for (const key of ['genexus', 'genexus18']) {
|
|
801
|
+
if (cfgObj.mcpServers[key]) {
|
|
802
|
+
delete cfgObj.mcpServers[key];
|
|
803
|
+
removedAny = true;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (!removedAny) return false;
|
|
807
|
+
writeClientJson(filePath, cfgObj);
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// VS Code native MCP lives in User\mcp.json and uses a top-level `servers` map
|
|
812
|
+
// with `type: "stdio"` (distinct from the `mcpServers` shape Claude/Cursor use).
|
|
813
|
+
function applyVsCodeServersJson(filePath, launcher, targetConfigPath) {
|
|
814
|
+
const parsed = fs.existsSync(filePath) ? readJsonFileSafe(filePath) : {};
|
|
815
|
+
if (parsed === null) throw new Error('Invalid JSON');
|
|
816
|
+
const cfgObj = parsed || {};
|
|
817
|
+
cfgObj.servers = cfgObj.servers || {};
|
|
818
|
+
cfgObj.servers.genexus = {
|
|
819
|
+
type: 'stdio',
|
|
820
|
+
...launcher,
|
|
821
|
+
env: { GX_CONFIG_PATH: targetConfigPath }
|
|
822
|
+
};
|
|
823
|
+
// Drop the legacy `genexus18` key written by older build-from-source installs.
|
|
824
|
+
if (cfgObj.servers.genexus18) delete cfgObj.servers.genexus18;
|
|
825
|
+
writeClientJson(filePath, cfgObj);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function removeVsCodeServersJson(filePath) {
|
|
829
|
+
const parsed = readJsonFileSafe(filePath);
|
|
830
|
+
if (parsed === null) throw new Error('Invalid JSON');
|
|
831
|
+
const cfgObj = parsed || {};
|
|
832
|
+
if (!cfgObj.servers) return false;
|
|
833
|
+
let removedAny = false;
|
|
834
|
+
for (const key of ['genexus', 'genexus18']) {
|
|
835
|
+
if (cfgObj.servers[key]) {
|
|
836
|
+
delete cfgObj.servers[key];
|
|
837
|
+
removedAny = true;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (!removedAny) return false;
|
|
841
|
+
writeClientJson(filePath, cfgObj);
|
|
480
842
|
return true;
|
|
481
843
|
}
|
|
482
844
|
|
|
@@ -485,6 +847,9 @@ function applyOpenCodeJson(filePath, launcher, targetConfigPath) {
|
|
|
485
847
|
const parsed = fs.existsSync(filePath) ? readJsonFileSafe(filePath) : {};
|
|
486
848
|
if (parsed === null) throw new Error('Invalid JSON');
|
|
487
849
|
const cfgObj = parsed || {};
|
|
850
|
+
// OpenCode configs carry a top-level $schema for editor validation; set it when
|
|
851
|
+
// absent (new file or a config that never had one) without clobbering a custom one.
|
|
852
|
+
if (!cfgObj.$schema) cfgObj.$schema = 'https://opencode.ai/config.json';
|
|
488
853
|
cfgObj.mcp = cfgObj.mcp || {};
|
|
489
854
|
cfgObj.mcp.genexus = {
|
|
490
855
|
type: 'local',
|
|
@@ -492,7 +857,8 @@ function applyOpenCodeJson(filePath, launcher, targetConfigPath) {
|
|
|
492
857
|
environment: { GX_CONFIG_PATH: targetConfigPath },
|
|
493
858
|
enabled: true
|
|
494
859
|
};
|
|
495
|
-
|
|
860
|
+
if (cfgObj.mcp.genexus18) delete cfgObj.mcp.genexus18;
|
|
861
|
+
writeClientJson(filePath, cfgObj);
|
|
496
862
|
}
|
|
497
863
|
|
|
498
864
|
function removeOpenCodeJson(filePath) {
|
|
@@ -501,7 +867,7 @@ function removeOpenCodeJson(filePath) {
|
|
|
501
867
|
const cfgObj = parsed || {};
|
|
502
868
|
if (!cfgObj.mcp || !cfgObj.mcp.genexus) return false;
|
|
503
869
|
delete cfgObj.mcp.genexus;
|
|
504
|
-
|
|
870
|
+
writeClientJson(filePath, cfgObj);
|
|
505
871
|
return true;
|
|
506
872
|
}
|
|
507
873
|
|
|
@@ -524,7 +890,7 @@ function applyCodexToml(filePath, launcher, targetConfigPath) {
|
|
|
524
890
|
lines.push('[mcp_servers.genexus.env]');
|
|
525
891
|
lines.push(`GX_CONFIG_PATH = ${tomlString(targetConfigPath)}`);
|
|
526
892
|
lines.push('');
|
|
527
|
-
|
|
893
|
+
writeClientText(filePath, stripped + lines.join('\n'));
|
|
528
894
|
}
|
|
529
895
|
|
|
530
896
|
function removeCodexToml(filePath) {
|
|
@@ -532,7 +898,7 @@ function removeCodexToml(filePath) {
|
|
|
532
898
|
const existing = fs.readFileSync(filePath, 'utf8');
|
|
533
899
|
const stripped = stripCodexGenexusBlocks(existing);
|
|
534
900
|
if (stripped === existing) return false;
|
|
535
|
-
|
|
901
|
+
writeClientText(filePath, stripped);
|
|
536
902
|
return true;
|
|
537
903
|
}
|
|
538
904
|
|
|
@@ -588,6 +954,7 @@ function normalizeExePath(p) {
|
|
|
588
954
|
}
|
|
589
955
|
|
|
590
956
|
function readClientCommandEntry(client) {
|
|
957
|
+
if (client.writeSupported === false) return null;
|
|
591
958
|
if (!fs.existsSync(client.path)) return null;
|
|
592
959
|
try {
|
|
593
960
|
if (client.format === 'mcpServers') {
|
|
@@ -604,6 +971,13 @@ function readClientCommandEntry(client) {
|
|
|
604
971
|
if (!entry || !Array.isArray(entry.command) || entry.command.length === 0) return null;
|
|
605
972
|
return { command: entry.command[0], args: entry.command.slice(1) };
|
|
606
973
|
}
|
|
974
|
+
if (client.format === 'vscode-servers') {
|
|
975
|
+
const parsed = readJsonFileSafe(client.path);
|
|
976
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
977
|
+
const entry = parsed.servers && parsed.servers.genexus;
|
|
978
|
+
if (!entry) return null;
|
|
979
|
+
return { command: entry.command || null, args: Array.isArray(entry.args) ? entry.args : [] };
|
|
980
|
+
}
|
|
607
981
|
if (client.format === 'codex-toml') {
|
|
608
982
|
const raw = fs.readFileSync(client.path, 'utf8');
|
|
609
983
|
// Minimal extraction: find [mcp_servers.genexus] block and pull command = "..."
|
|
@@ -786,6 +1160,8 @@ module.exports = {
|
|
|
786
1160
|
patchClientConfig,
|
|
787
1161
|
unpatchClientConfig,
|
|
788
1162
|
getClientConfigTargets,
|
|
1163
|
+
detectClientInstalled,
|
|
1164
|
+
clientsStatus,
|
|
789
1165
|
listSupportedClientIds,
|
|
790
1166
|
filterClientTargets,
|
|
791
1167
|
getLocalAppDataCacheDir,
|
package/cli/run.test.js
CHANGED
|
@@ -6,6 +6,7 @@ const os = require('node:os');
|
|
|
6
6
|
const fs = require('node:fs');
|
|
7
7
|
const { renderOutput } = require('./lib/output');
|
|
8
8
|
const { compareSemver } = require('./lib/update-check');
|
|
9
|
+
const { detectClientInstalled, readJsonFileSafe } = require('./lib/config');
|
|
9
10
|
|
|
10
11
|
const cliPath = path.join(__dirname, 'run.js');
|
|
11
12
|
|
|
@@ -599,6 +600,227 @@ test('update --help returns usage entry', () => {
|
|
|
599
600
|
assert.ok(parsed.ok.usage.includes('genexus-mcp update'));
|
|
600
601
|
});
|
|
601
602
|
|
|
603
|
+
test('detectClientInstalled flags an agent installed via marker even with no MCP config', () => {
|
|
604
|
+
// Regression for the field report where Antigravity showed "not detected":
|
|
605
|
+
// an agent whose install dir exists but which hasn't created our MCP config
|
|
606
|
+
// file yet must still be detected as installed.
|
|
607
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-detect-'));
|
|
608
|
+
const installDir = path.join(tempRoot, 'Programs', 'Antigravity');
|
|
609
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
610
|
+
|
|
611
|
+
const client = {
|
|
612
|
+
name: 'Antigravity',
|
|
613
|
+
path: path.join(tempRoot, 'never-created', 'mcp_config.json'),
|
|
614
|
+
installMarkers: [installDir]
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const det = detectClientInstalled(client);
|
|
618
|
+
assert.equal(det.installed, true, 'marker dir present => installed');
|
|
619
|
+
assert.equal(det.hasConfig, false, 'config file does not exist yet');
|
|
620
|
+
assert.equal(det.markerHit, installDir);
|
|
621
|
+
|
|
622
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test('detectClientInstalled reports not-installed and lists checked paths', () => {
|
|
626
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-detect-'));
|
|
627
|
+
const client = {
|
|
628
|
+
name: 'Antigravity',
|
|
629
|
+
path: path.join(tempRoot, 'nope.json'),
|
|
630
|
+
installMarkers: [path.join(tempRoot, 'absent-a'), path.join(tempRoot, 'absent-b')]
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const det = detectClientInstalled(client);
|
|
634
|
+
assert.equal(det.installed, false);
|
|
635
|
+
assert.equal(det.markerHit, null);
|
|
636
|
+
assert.deepEqual(det.markersChecked, client.installMarkers);
|
|
637
|
+
|
|
638
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test('detectClientInstalled treats an existing config file as installed', () => {
|
|
642
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-detect-'));
|
|
643
|
+
const cfg = path.join(tempRoot, 'settings.json');
|
|
644
|
+
fs.writeFileSync(cfg, '{}');
|
|
645
|
+
const client = { name: 'Gemini CLI', path: cfg, installMarkers: [] };
|
|
646
|
+
|
|
647
|
+
const det = detectClientInstalled(client);
|
|
648
|
+
assert.equal(det.installed, true);
|
|
649
|
+
assert.equal(det.hasConfig, true);
|
|
650
|
+
|
|
651
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// Build a throwaway HOME so client-config writes never touch the real machine.
|
|
655
|
+
function sandboxHomeEnv(root) {
|
|
656
|
+
return {
|
|
657
|
+
HOME: root,
|
|
658
|
+
USERPROFILE: root,
|
|
659
|
+
APPDATA: path.join(root, 'AppData', 'Roaming'),
|
|
660
|
+
LOCALAPPDATA: path.join(root, 'AppData', 'Local'),
|
|
661
|
+
XDG_CONFIG_HOME: path.join(root, '.config')
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
test('clients list returns structured status with summary', () => {
|
|
666
|
+
const result = runCli(['clients', '--format', 'json']);
|
|
667
|
+
assert.equal(result.status, 0);
|
|
668
|
+
const parsed = JSON.parse(result.stdout);
|
|
669
|
+
assert.equal(parsed.meta.command, 'clients.list');
|
|
670
|
+
assert.ok(Array.isArray(parsed.ok.clients));
|
|
671
|
+
assert.ok(parsed.ok.clients.length >= 8);
|
|
672
|
+
assert.equal(typeof parsed.ok.summary.installed, 'number');
|
|
673
|
+
assert.equal(typeof parsed.ok.summary.registered, 'number');
|
|
674
|
+
const row = parsed.ok.clients.find((c) => c.id === 'antigravity');
|
|
675
|
+
assert.ok(row, 'antigravity should be listed');
|
|
676
|
+
assert.equal(typeof row.installed, 'boolean');
|
|
677
|
+
assert.equal(typeof row.registered, 'boolean');
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
test('clients add registers a client into a sandbox home with backup + atomic write', () => {
|
|
681
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-clients-'));
|
|
682
|
+
const env = sandboxHomeEnv(tempRoot);
|
|
683
|
+
const cfgPath = path.join(tempRoot, 'config.json');
|
|
684
|
+
fs.writeFileSync(cfgPath, JSON.stringify({ Environment: { KBPath: tempRoot } }));
|
|
685
|
+
|
|
686
|
+
// Pre-existing cursor config so we can assert a backup is taken.
|
|
687
|
+
const cursorCfg = path.join(tempRoot, '.cursor', 'mcp.json');
|
|
688
|
+
fs.mkdirSync(path.dirname(cursorCfg), { recursive: true });
|
|
689
|
+
fs.writeFileSync(cursorCfg, JSON.stringify({ mcpServers: { other: { command: 'x' } } }, null, 2));
|
|
690
|
+
|
|
691
|
+
const res = runCli(['clients', 'add', '--clients', 'cursor', '--format', 'json'], {
|
|
692
|
+
env: { ...env, GX_CONFIG_PATH: cfgPath }
|
|
693
|
+
});
|
|
694
|
+
assert.equal(res.status, 0);
|
|
695
|
+
const parsed = JSON.parse(res.stdout);
|
|
696
|
+
assert.equal(parsed.meta.command, 'clients.add');
|
|
697
|
+
assert.ok(parsed.ok.patchedClients.includes('Cursor'));
|
|
698
|
+
|
|
699
|
+
const written = JSON.parse(fs.readFileSync(cursorCfg, 'utf8'));
|
|
700
|
+
assert.ok(written.mcpServers.genexus, 'genexus entry should be written');
|
|
701
|
+
assert.ok(written.mcpServers.other, 'pre-existing entries preserved');
|
|
702
|
+
|
|
703
|
+
const baks = fs.readdirSync(path.dirname(cursorCfg)).filter((f) => f.includes('.bak'));
|
|
704
|
+
assert.ok(baks.length >= 1, 'a .bak backup should be created before mutating');
|
|
705
|
+
|
|
706
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test('clients add tolerates a JSONC (commented) VS Code mcp.json', () => {
|
|
710
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-jsonc-'));
|
|
711
|
+
const env = sandboxHomeEnv(tempRoot);
|
|
712
|
+
const cfgPath = path.join(tempRoot, 'config.json');
|
|
713
|
+
fs.writeFileSync(cfgPath, JSON.stringify({ Environment: { KBPath: tempRoot } }));
|
|
714
|
+
|
|
715
|
+
const vscodeCfg = path.join(env.APPDATA, 'Code', 'User', 'mcp.json');
|
|
716
|
+
fs.mkdirSync(path.dirname(vscodeCfg), { recursive: true });
|
|
717
|
+
fs.writeFileSync(vscodeCfg, '{\n // user comment\n "servers": {\n "foo": { "command": "bar" },\n }\n}\n');
|
|
718
|
+
|
|
719
|
+
const res = runCli(['clients', 'add', '--clients', 'vscode', '--format', 'json'], {
|
|
720
|
+
env: { ...env, GX_CONFIG_PATH: cfgPath }
|
|
721
|
+
});
|
|
722
|
+
assert.equal(res.status, 0);
|
|
723
|
+
const parsed = JSON.parse(res.stdout);
|
|
724
|
+
assert.ok(parsed.ok.patchedClients.includes('VS Code'), 'VS Code should be patched despite comments');
|
|
725
|
+
|
|
726
|
+
const written = JSON.parse(fs.readFileSync(vscodeCfg, 'utf8'));
|
|
727
|
+
assert.ok(written.servers.genexus, 'genexus server entry written');
|
|
728
|
+
assert.ok(written.servers.foo, 'pre-existing server preserved');
|
|
729
|
+
|
|
730
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
test('clients add replaces a legacy genexus18 entry instead of duplicating it', () => {
|
|
734
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-legacy-'));
|
|
735
|
+
const env = sandboxHomeEnv(tempRoot);
|
|
736
|
+
const cfgPath = path.join(tempRoot, 'config.json');
|
|
737
|
+
fs.writeFileSync(cfgPath, JSON.stringify({ Environment: { KBPath: tempRoot } }));
|
|
738
|
+
|
|
739
|
+
const cursorCfg = path.join(tempRoot, '.cursor', 'mcp.json');
|
|
740
|
+
fs.mkdirSync(path.dirname(cursorCfg), { recursive: true });
|
|
741
|
+
fs.writeFileSync(cursorCfg, JSON.stringify({ mcpServers: { genexus18: { command: 'C:\\old\\start_mcp.bat' } } }, null, 2));
|
|
742
|
+
|
|
743
|
+
const res = runCli(['clients', 'add', '--clients', 'cursor', '--format', 'json'], {
|
|
744
|
+
env: { ...env, GX_CONFIG_PATH: cfgPath }
|
|
745
|
+
});
|
|
746
|
+
assert.equal(res.status, 0);
|
|
747
|
+
|
|
748
|
+
const written = JSON.parse(fs.readFileSync(cursorCfg, 'utf8'));
|
|
749
|
+
assert.ok(written.mcpServers.genexus, 'new genexus entry present');
|
|
750
|
+
assert.equal(written.mcpServers.genexus18, undefined, 'legacy genexus18 removed (no duplicate)');
|
|
751
|
+
|
|
752
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
test('clients list flags a registered command pointing at a missing launcher as stale (.bat too)', () => {
|
|
756
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-stale-'));
|
|
757
|
+
const env = sandboxHomeEnv(tempRoot);
|
|
758
|
+
const cursorCfg = path.join(tempRoot, '.cursor', 'mcp.json');
|
|
759
|
+
fs.mkdirSync(path.dirname(cursorCfg), { recursive: true });
|
|
760
|
+
// A non-.exe launcher (.bat) that no longer exists must also be flagged stale.
|
|
761
|
+
const missing = path.join(tempRoot, 'gone', 'start_mcp.bat');
|
|
762
|
+
fs.writeFileSync(cursorCfg, JSON.stringify({ mcpServers: { genexus: { command: missing, args: [] } } }, null, 2));
|
|
763
|
+
|
|
764
|
+
const res = runCli(['clients', '--format', 'json'], { env });
|
|
765
|
+
assert.equal(res.status, 0);
|
|
766
|
+
const parsed = JSON.parse(res.stdout);
|
|
767
|
+
const cursor = parsed.ok.clients.find((c) => c.id === 'cursor');
|
|
768
|
+
assert.ok(cursor.registered, 'cursor should read as registered');
|
|
769
|
+
assert.equal(cursor.commandStale, true, 'missing launcher => stale');
|
|
770
|
+
assert.ok(parsed.help.some((h) => h.includes('missing gateway exe')), 'help should call out the stale client');
|
|
771
|
+
|
|
772
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test('readJsonFileSafe parses JSONC without corrupting string values containing commas', () => {
|
|
776
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-jsonc2-'));
|
|
777
|
+
const f = path.join(tempRoot, 'mcp.json');
|
|
778
|
+
// Leading comment forces the JSONC fallback; the string value contains ", ]"
|
|
779
|
+
// and the object has a legitimate trailing comma that SHOULD be stripped.
|
|
780
|
+
fs.writeFileSync(f, '{\n // comment\n "servers": { "x": { "command": "a, ]b", "args": ["c,]"] } },\n}\n');
|
|
781
|
+
const parsed = readJsonFileSafe(f);
|
|
782
|
+
assert.ok(parsed, 'should parse');
|
|
783
|
+
assert.equal(parsed.servers.x.command, 'a, ]b', 'comma inside string value preserved');
|
|
784
|
+
assert.deepEqual(parsed.servers.x.args, ['c,]'], 'comma inside array string preserved');
|
|
785
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
test('readJsonFileSafe strips a genuine trailing comma', () => {
|
|
789
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-jsonc3-'));
|
|
790
|
+
const f = path.join(tempRoot, 'mcp.json');
|
|
791
|
+
fs.writeFileSync(f, '{\n // c\n "a": [1, 2, 3,],\n "b": { "x": 1, },\n}\n');
|
|
792
|
+
const parsed = readJsonFileSafe(f);
|
|
793
|
+
assert.deepEqual(parsed.a, [1, 2, 3]);
|
|
794
|
+
assert.deepEqual(parsed.b, { x: 1 });
|
|
795
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test('clients add without --clients is a usage error', () => {
|
|
799
|
+
const res = runCli(['clients', 'add', '--format', 'json']);
|
|
800
|
+
assert.equal(res.status, 2);
|
|
801
|
+
const parsed = JSON.parse(res.stdout);
|
|
802
|
+
assert.equal(parsed.error.code, 'usage_error');
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
test('clients remove drops the genexus entry (sandbox home)', () => {
|
|
806
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-rm-'));
|
|
807
|
+
const env = sandboxHomeEnv(tempRoot);
|
|
808
|
+
const cursorCfg = path.join(tempRoot, '.cursor', 'mcp.json');
|
|
809
|
+
fs.mkdirSync(path.dirname(cursorCfg), { recursive: true });
|
|
810
|
+
fs.writeFileSync(cursorCfg, JSON.stringify({ mcpServers: { genexus: { command: 'npx' }, genexus18: { command: 'old' } } }, null, 2));
|
|
811
|
+
|
|
812
|
+
const res = runCli(['clients', 'remove', '--clients', 'cursor', '--format', 'json'], { env });
|
|
813
|
+
assert.equal(res.status, 0);
|
|
814
|
+
const parsed = JSON.parse(res.stdout);
|
|
815
|
+
assert.ok(parsed.ok.removedClients.includes('Cursor'));
|
|
816
|
+
|
|
817
|
+
const written = JSON.parse(fs.readFileSync(cursorCfg, 'utf8'));
|
|
818
|
+
assert.equal(written.mcpServers.genexus, undefined, 'genexus removed');
|
|
819
|
+
assert.equal(written.mcpServers.genexus18, undefined, 'legacy genexus18 also removed');
|
|
820
|
+
|
|
821
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
822
|
+
});
|
|
823
|
+
|
|
602
824
|
test('compareSemver detects newer, older, equal versions', () => {
|
|
603
825
|
assert.equal(compareSemver('1.3.1', '1.3.0'), 1);
|
|
604
826
|
assert.equal(compareSemver('v1.4.0', '1.3.9'), 1);
|
|
@@ -27,6 +27,7 @@ Entry points:
|
|
|
27
27
|
- `genexus-mcp llm help`
|
|
28
28
|
- `genexus-mcp status`
|
|
29
29
|
- `genexus-mcp doctor --mcp-smoke`
|
|
30
|
+
- `genexus-mcp clients` (which AI agents are installed/registered; `clients add|remove --clients <csv>`)
|
|
30
31
|
- `genexus-mcp tools list`
|
|
31
32
|
- `genexus-mcp config show`
|
|
32
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genexus-mcp",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.3",
|
|
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.8.
|
|
10
|
+
"GxMcp.Gateway/2.8.3": {
|
|
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.8.
|
|
69
|
+
"GxMcp.Gateway/2.8.3": {
|
|
70
70
|
"type": "project",
|
|
71
71
|
"serviceable": false,
|
|
72
72
|
"sha512": ""
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|