genexus-mcp 2.50.0 → 2.51.0

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.
@@ -0,0 +1,189 @@
1
+ const fs = require('fs');
2
+
3
+ class BaseClientAdapter {
4
+ apply(_client, _launcher, _targetConfigPath, _opts = {}) {
5
+ throw new Error('apply() must be implemented by subclass');
6
+ }
7
+ remove(_client, _opts = {}) {
8
+ throw new Error('remove() must be implemented by subclass');
9
+ }
10
+ read(_client, _serverName) {
11
+ throw new Error('read() must be implemented by subclass');
12
+ }
13
+ }
14
+
15
+ class McpServersJsonAdapter extends BaseClientAdapter {
16
+ constructor(configModule) {
17
+ super();
18
+ this.config = configModule || require('./config');
19
+ }
20
+ apply(client, launcher, targetConfigPath, opts = {}) {
21
+ return this.config.applyMcpServersJson(client.path, launcher, targetConfigPath, opts);
22
+ }
23
+ remove(client, opts = {}) {
24
+ return this.config.removeMcpServersJson(client.path, opts);
25
+ }
26
+ read(client, serverName) {
27
+ if (!fs.existsSync(client.path)) return null;
28
+ const parsed = this.config.readJsonFileSafe(client.path);
29
+ if (!parsed || typeof parsed !== 'object') return null;
30
+ let entry = parsed.mcpServers && parsed.mcpServers[serverName];
31
+ if (!entry && serverName === 'genexus18mcp') {
32
+ const legacyGx = parsed.mcpServers && parsed.mcpServers.genexus;
33
+ if (legacyGx && !this.config.isThirdPartyMcpEntry(legacyGx)) entry = legacyGx;
34
+ else if (parsed.mcpServers && parsed.mcpServers.genexus18) entry = parsed.mcpServers.genexus18;
35
+ }
36
+ if (!entry) return null;
37
+ return {
38
+ command: entry.command,
39
+ args: Array.isArray(entry.args) ? entry.args : [],
40
+ env: (entry.env && typeof entry.env === 'object') ? entry.env : {}
41
+ };
42
+ }
43
+ }
44
+
45
+ class VsCodeServersAdapter extends BaseClientAdapter {
46
+ constructor(configModule) {
47
+ super();
48
+ this.config = configModule || require('./config');
49
+ }
50
+ apply(client, launcher, targetConfigPath, opts = {}) {
51
+ return this.config.applyVsCodeServersJson(client.path, launcher, targetConfigPath, opts);
52
+ }
53
+ remove(client, opts = {}) {
54
+ return this.config.removeVsCodeServersJson(client.path, opts);
55
+ }
56
+ read(client, serverName) {
57
+ if (!fs.existsSync(client.path)) return null;
58
+ const parsed = this.config.readJsonFileSafe(client.path);
59
+ if (!parsed || typeof parsed !== 'object') return null;
60
+ let entry = parsed.servers && parsed.servers[serverName];
61
+ if (!entry && serverName === 'genexus18mcp') {
62
+ const legacyGx = parsed.servers && parsed.servers.genexus;
63
+ if (legacyGx && !this.config.isThirdPartyMcpEntry(legacyGx)) entry = legacyGx;
64
+ else if (parsed.servers && parsed.servers.genexus18) entry = parsed.servers.genexus18;
65
+ }
66
+ if (!entry) return null;
67
+ return {
68
+ command: entry.command,
69
+ args: Array.isArray(entry.args) ? entry.args : [],
70
+ env: (entry.env && typeof entry.env === 'object') ? entry.env : {}
71
+ };
72
+ }
73
+ }
74
+
75
+ class OpenCodeJsoncAdapter extends BaseClientAdapter {
76
+ constructor(configModule) {
77
+ super();
78
+ this.config = configModule || require('./config');
79
+ }
80
+ apply(client, launcher, targetConfigPath, opts = {}) {
81
+ return this.config.applyOpenCodeJson(client.path, launcher, targetConfigPath, opts);
82
+ }
83
+ remove(client, opts = {}) {
84
+ return this.config.removeOpenCodeJson(client.path, opts);
85
+ }
86
+ read(client, serverName) {
87
+ if (!fs.existsSync(client.path)) return null;
88
+ const parsed = this.config.readJsonFileSafe(client.path);
89
+ if (!parsed || typeof parsed !== 'object' || !parsed.mcp || typeof parsed.mcp !== 'object') return null;
90
+ const nested = parsed.mcp.servers && typeof parsed.mcp.servers === 'object' && !Array.isArray(parsed.mcp.servers);
91
+ const container = nested ? parsed.mcp.servers : parsed.mcp;
92
+ let entry = container[serverName];
93
+ if (!entry && serverName === 'genexus18mcp') {
94
+ const legacyGx = container.genexus;
95
+ if (legacyGx && !this.config.isThirdPartyMcpEntry(legacyGx)) entry = legacyGx;
96
+ else if (container.genexus18) entry = container.genexus18;
97
+ }
98
+ if (!entry) return null;
99
+ const cmd = Array.isArray(entry.command) ? entry.command[0] : entry.command;
100
+ const args = Array.isArray(entry.command) ? entry.command.slice(1) : (Array.isArray(entry.args) ? entry.args : []);
101
+ const env = (entry.environment && typeof entry.environment === 'object') ? entry.environment
102
+ : ((entry.env && typeof entry.env === 'object') ? entry.env : {});
103
+ return { command: cmd, args, env };
104
+ }
105
+ }
106
+
107
+ class CodexTomlAdapter extends BaseClientAdapter {
108
+ constructor(configModule) {
109
+ super();
110
+ this.config = configModule || require('./config');
111
+ }
112
+ apply(client, launcher, targetConfigPath, opts = {}) {
113
+ return this.config.applyCodexToml(client.path, launcher, targetConfigPath, opts);
114
+ }
115
+ remove(client, opts = {}) {
116
+ return this.config.removeCodexToml(client.path, opts);
117
+ }
118
+ read(client, serverName) {
119
+ if (!fs.existsSync(client.path)) return null;
120
+ let content = '';
121
+ try {
122
+ content = fs.readFileSync(client.path, 'utf8');
123
+ } catch {
124
+ return null;
125
+ }
126
+ let parsed = this.config.extractCodexTomlEntry(content, serverName);
127
+ if (!parsed && serverName === 'genexus18mcp') {
128
+ const legacyGx = this.config.extractCodexTomlEntry(content, 'genexus');
129
+ if (legacyGx && !this.config.isThirdPartyMcpEntry(legacyGx)) parsed = legacyGx;
130
+ else {
131
+ const legacyGx18 = this.config.extractCodexTomlEntry(content, 'genexus18');
132
+ if (legacyGx18) parsed = legacyGx18;
133
+ }
134
+ }
135
+ if (!parsed) return null;
136
+ return {
137
+ command: parsed.command,
138
+ args: Array.isArray(parsed.args) ? parsed.args : [],
139
+ env: (parsed.env && typeof parsed.env === 'object') ? parsed.env : {}
140
+ };
141
+ }
142
+ }
143
+
144
+ const adapters = {
145
+ 'mcpServers': new McpServersJsonAdapter(),
146
+ 'vscode-servers': new VsCodeServersAdapter(),
147
+ 'opencode': new OpenCodeJsoncAdapter(),
148
+ 'codex-toml': new CodexTomlAdapter()
149
+ };
150
+
151
+ function getClientAdapter(format) {
152
+ const adapter = adapters[format];
153
+ if (!adapter) {
154
+ throw new Error(`Unknown client format: ${format}`);
155
+ }
156
+ return adapter;
157
+ }
158
+
159
+ class ClientConfigManager {
160
+ constructor(configModule) {
161
+ this.config = configModule || require('./config');
162
+ }
163
+
164
+ apply(client, launcher, targetConfigPath, opts = {}) {
165
+ const adapter = getClientAdapter(client.format);
166
+ return adapter.apply(client, launcher, targetConfigPath, opts);
167
+ }
168
+
169
+ remove(client, opts = {}) {
170
+ const adapter = getClientAdapter(client.format);
171
+ return adapter.remove(client, opts);
172
+ }
173
+
174
+ read(client, serverName) {
175
+ if (client.writeSupported === false) return null;
176
+ const adapter = getClientAdapter(client.format);
177
+ return adapter.read(client, serverName);
178
+ }
179
+ }
180
+
181
+ module.exports = {
182
+ BaseClientAdapter,
183
+ McpServersJsonAdapter,
184
+ VsCodeServersAdapter,
185
+ OpenCodeJsoncAdapter,
186
+ CodexTomlAdapter,
187
+ getClientAdapter,
188
+ ClientConfigManager
189
+ };
@@ -0,0 +1,50 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const path = require('node:path');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const {
7
+ getClientAdapter,
8
+ McpServersJsonAdapter,
9
+ VsCodeServersAdapter,
10
+ OpenCodeJsoncAdapter,
11
+ CodexTomlAdapter,
12
+ ClientConfigManager
13
+ } = require('./client-adapters');
14
+
15
+ test('getClientAdapter returns correct strategy for each format', () => {
16
+ assert.ok(getClientAdapter('mcpServers') instanceof McpServersJsonAdapter);
17
+ assert.ok(getClientAdapter('vscode-servers') instanceof VsCodeServersAdapter);
18
+ assert.ok(getClientAdapter('opencode') instanceof OpenCodeJsoncAdapter);
19
+ assert.ok(getClientAdapter('codex-toml') instanceof CodexTomlAdapter);
20
+ });
21
+
22
+ test('ClientConfigManager applies and reads mcpServers format cleanly', () => {
23
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gx-cli-test-'));
24
+ const configPath = path.join(tmpDir, 'mcp.json');
25
+ const client = { id: 'test-cursor', name: 'Test Cursor', format: 'mcpServers', path: configPath };
26
+ const launcher = { command: 'node', args: ['run.js'] };
27
+
28
+ try {
29
+ const manager = new ClientConfigManager();
30
+ manager.apply(client, launcher, 'C:\\MyKb\\config.json', { serverName: 'genexus18mcp' });
31
+
32
+ const raw = fs.readFileSync(configPath, 'utf8');
33
+ const parsed = JSON.parse(raw);
34
+ assert.ok(parsed.mcpServers);
35
+ assert.ok(parsed.mcpServers.genexus18mcp);
36
+ assert.equal(parsed.mcpServers.genexus18mcp.command, 'node');
37
+
38
+ const readEntry = manager.read(client, 'genexus18mcp');
39
+ assert.ok(readEntry);
40
+ assert.equal(readEntry.command, 'node');
41
+
42
+ const wasRemoved = manager.remove(client, { serverName: 'genexus18mcp' });
43
+ assert.equal(wasRemoved, true);
44
+
45
+ const afterRemove = JSON.parse(fs.readFileSync(configPath, 'utf8'));
46
+ assert.equal(afterRemove.mcpServers.genexus18mcp, undefined);
47
+ } finally {
48
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { }
49
+ }
50
+ });
package/cli/lib/config.js CHANGED
@@ -861,33 +861,13 @@ function unpatchClientConfig(opts = {}) {
861
861
  }
862
862
 
863
863
  function applyClientEntry(client, launcher, targetConfigPath, opts = {}) {
864
- switch (client.format) {
865
- case 'mcpServers':
866
- return applyMcpServersJson(client.path, launcher, targetConfigPath, opts);
867
- case 'opencode':
868
- return applyOpenCodeJson(client.path, launcher, targetConfigPath, opts);
869
- case 'codex-toml':
870
- return applyCodexToml(client.path, launcher, targetConfigPath, opts);
871
- case 'vscode-servers':
872
- return applyVsCodeServersJson(client.path, launcher, targetConfigPath, opts);
873
- default:
874
- throw new Error(`Unknown client format: ${client.format}`);
875
- }
864
+ const { getClientAdapter } = require('./client-adapters');
865
+ return getClientAdapter(client.format).apply(client, launcher, targetConfigPath, opts);
876
866
  }
877
867
 
878
868
  function removeClientEntry(client, opts = {}) {
879
- switch (client.format) {
880
- case 'mcpServers':
881
- return removeMcpServersJson(client.path, opts);
882
- case 'opencode':
883
- return removeOpenCodeJson(client.path, opts);
884
- case 'codex-toml':
885
- return removeCodexToml(client.path, opts);
886
- case 'vscode-servers':
887
- return removeVsCodeServersJson(client.path, opts);
888
- default:
889
- throw new Error(`Unknown client format: ${client.format}`);
890
- }
869
+ const { getClientAdapter } = require('./client-adapters');
870
+ return getClientAdapter(client.format).remove(client, opts);
891
871
  }
892
872
 
893
873
  function applyMcpServersJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false } = {}) {
@@ -1534,5 +1514,17 @@ module.exports = {
1534
1514
  getLauncher,
1535
1515
  readClientCommandEntry,
1536
1516
  isOurMcpEntry,
1537
- DEFAULT_MCP_SERVER_NAME
1517
+ DEFAULT_MCP_SERVER_NAME,
1518
+ applyMcpServersJson,
1519
+ removeMcpServersJson,
1520
+ applyVsCodeServersJson,
1521
+ removeVsCodeServersJson,
1522
+ applyOpenCodeJson,
1523
+ removeOpenCodeJson,
1524
+ applyCodexToml,
1525
+ removeCodexToml,
1526
+ extractCodexTomlEntry,
1527
+ isThirdPartyMcpEntry,
1528
+ applyClientEntry,
1529
+ removeClientEntry
1538
1530
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genexus-mcp",
3
- "version": "2.50.0",
3
+ "version": "2.51.0",
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": [
@@ -30,7 +30,7 @@
30
30
  "automation"
31
31
  ],
32
32
  "scripts": {
33
- "test": "node --test cli/run.test.js",
33
+ "test": "node --test cli/run.test.js cli/lib/client-adapters.test.js",
34
34
  "lint": "eslint cli scripts eslint.config.js --max-warnings=0",
35
35
  "test:one": "node scripts/test-one.js",
36
36
  "prepack": "node scripts/clean-package.js",
@@ -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.50.0": {
10
+ "GxMcp.Gateway/2.51.0": {
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.50.0": {
69
+ "GxMcp.Gateway/2.51.0": {
70
70
  "type": "project",
71
71
  "serviceable": false,
72
72
  "sha512": ""
Binary file
Binary file
@@ -1,17 +1,17 @@
1
1
  {
2
- "Logging": {
3
- "Level": "Debug",
4
- "Path": "logs"
5
- },
6
- "Server": {
7
- "McpStdio": true,
8
- "HttpPort": 5000
2
+ "Environment": {
3
+ "KBPath": "C:\\\\KBs\\\\YourKB"
9
4
  },
10
5
  "GeneXus": {
11
6
  "WorkerExecutable": "C:\\Projetos\\Genexus18MCP\\publish\\\\worker\\\\GxMcp.Worker.exe",
12
7
  "InstallationPath": "C:\\\\Program Files (x86)\\\\GeneXus\\\\GeneXus18"
13
8
  },
14
- "Environment": {
15
- "KBPath": "C:\\\\KBs\\\\YourKB"
9
+ "Server": {
10
+ "McpStdio": true,
11
+ "HttpPort": 5000
12
+ },
13
+ "Logging": {
14
+ "Level": "Debug",
15
+ "Path": "logs"
16
16
  }
17
17
  }
Binary file