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/cli/run.test.js CHANGED
@@ -5,7 +5,8 @@ const path = require('node:path');
5
5
  const os = require('node:os');
6
6
  const fs = require('node:fs');
7
7
  const { renderOutput } = require('./lib/output');
8
- const { compareSemver } = require('./lib/update-check');
8
+ const { compareSemver, detectInstallMethod, upgradePlanFor } = 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);
@@ -607,6 +829,36 @@ test('compareSemver detects newer, older, equal versions', () => {
607
829
  assert.equal(compareSemver('garbage', '1.0.0'), 0);
608
830
  });
609
831
 
832
+ test('detectInstallMethod returns fixed-path when GENEXUS_MCP_GATEWAY_EXE is set', () => {
833
+ const prev = process.env.GENEXUS_MCP_GATEWAY_EXE;
834
+ process.env.GENEXUS_MCP_GATEWAY_EXE = 'C:\\Tools\\GenexusMCP\\GxMcp.Gateway.exe';
835
+ try {
836
+ const r = detectInstallMethod();
837
+ assert.equal(r.method, 'fixed-path');
838
+ assert.equal(r.detail, 'C:\\Tools\\GenexusMCP\\GxMcp.Gateway.exe');
839
+ } finally {
840
+ if (prev === undefined) delete process.env.GENEXUS_MCP_GATEWAY_EXE;
841
+ else process.env.GENEXUS_MCP_GATEWAY_EXE = prev;
842
+ }
843
+ });
844
+
845
+ test('upgradePlanFor encodes the per-method upgrade strategy', () => {
846
+ const npx = upgradePlanFor('npx-latest', 'latest');
847
+ assert.equal(npx.auto, true, 'npx@latest auto-updates on restart');
848
+ assert.ok(npx.steps.join(' ').toLowerCase().includes('restart'));
849
+
850
+ const npm = upgradePlanFor('npm-global', 'latest');
851
+ assert.equal(npm.auto, false);
852
+ assert.deepEqual(npm.applyCommand.args, ['install', '-g', 'genexus-mcp@latest']);
853
+
854
+ const npmNext = upgradePlanFor('npm-global', 'next');
855
+ assert.deepEqual(npmNext.applyCommand.args, ['install', '-g', 'genexus-mcp@next']);
856
+
857
+ const fixed = upgradePlanFor('fixed-path', 'latest');
858
+ assert.equal(fixed.auto, false);
859
+ assert.equal(fixed.applyCommand, null, 'fixed-path has no npm apply; uses the installer');
860
+ });
861
+
610
862
  test('gateway passthrough remains intact when no AXI subcommand is used', () => {
611
863
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-test-'));
612
864
  const fakeGateway = path.join(tempRoot, 'fake-gateway.js');
@@ -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.2",
3
+ "version": "2.8.4",
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.2": {
10
+ "GxMcp.Gateway/2.8.4": {
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.2": {
69
+ "GxMcp.Gateway/2.8.4": {
70
70
  "type": "project",
71
71
  "serviceable": false,
72
72
  "sha512": ""
Binary file
Binary file
@@ -1,20 +1,17 @@
1
- {
2
- "GeneXus": {
3
- "InstallationPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus18",
4
- "WorkerExecutable": "C:\\Projetos\\Genexus18MCP\\publish\\worker\\GxMcp.Worker.exe"
5
- },
6
- "Server": {
7
- "HttpPort": 5000,
8
- "McpStdio": true,
9
- "BindAddress": "127.0.0.1"
10
- },
11
- "Logging": {
12
- "Level": "Debug",
13
- "Path": "logs"
14
- },
15
- "Environment": {
16
- "KBPath": "C:\\KBs\\AcademicoHomolog1",
17
- "GX_SHADOW_PATH": "C:\\Projetos\\Genexus18MCP\\.gx_mirror",
18
- "DefaultKb": "academicohomolog1"
19
- }
20
- }
1
+ {
2
+ "GeneXus": {
3
+ "WorkerExecutable": "C:\\Projetos\\Genexus18MCP\\publish\\\\worker\\\\GxMcp.Worker.exe",
4
+ "InstallationPath": "C:\\\\Program Files (x86)\\\\GeneXus\\\\GeneXus18"
5
+ },
6
+ "Server": {
7
+ "HttpPort": 5000,
8
+ "McpStdio": true
9
+ },
10
+ "Logging": {
11
+ "Path": "logs",
12
+ "Level": "Debug"
13
+ },
14
+ "Environment": {
15
+ "KBPath": "C:\\\\KBs\\\\YourKB"
16
+ }
17
+ }
Binary file
Binary file
Binary file