genexus-mcp 2.8.2 → 2.8.4
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 +151 -8
- package/cli/index.js +24 -1
- package/cli/lib/config.js +395 -19
- package/cli/lib/update-check.js +241 -74
- package/cli/run.test.js +253 -1
- 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/config.json +17 -20
- package/publish/worker/GxMcp.Worker.exe +0 -0
- package/publish/GxMcp.Gateway.pdb +0 -0
- package/publish/worker/GxMcp.Worker.pdb +0 -0
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,
|