genexus-mcp 3.2.1 → 3.2.2

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
@@ -15,7 +15,8 @@ const {
15
15
  getGeneXusCatalogEntries,
16
16
  readGeneXusInstallationIdentity,
17
17
  readGeneXusKbIdentity,
18
- compareGeneXusKbAndInstallation
18
+ compareGeneXusKbAndInstallation,
19
+ patchClientConfig
19
20
  } = require('./lib/config');
20
21
  const { handleInit } = require('./commands/axi');
21
22
 
@@ -177,6 +178,129 @@ test('non-interactive init supports idempotent no-op', () => {
177
178
  fs.rmSync(tempRoot, { recursive: true, force: true });
178
179
  });
179
180
 
181
+ test('config create creates explicit neutral runtime without KB or client registration', () => {
182
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-neutral-'));
183
+ const output = path.join(tempRoot, 'nested', 'runtime.json');
184
+ const result = runCli([
185
+ 'config', 'create', '--config-scope', 'neutral', '--output', output,
186
+ '--gx', 'C:\\GeneXus18', '--worker', 'C:\\worker.exe',
187
+ '--gateway-mode', 'stdio-isolated', '--resolution-policy', 'strict', '--format', 'json'
188
+ ]);
189
+ assert.equal(result.status, 0);
190
+ const parsed = JSON.parse(result.stdout);
191
+ assert.equal(parsed.meta.command, 'config.create');
192
+ assert.equal(parsed.ok.configPath, path.resolve(output));
193
+ assert.equal(parsed.ok.clientsPatchedCount, 0);
194
+ assert.equal(parsed.ok.config.Environment.KBPath, undefined);
195
+ assert.equal(parsed.ok.config.Environment.KBs, undefined);
196
+ assert.equal(parsed.meta.clientRegistration, 'not_attempted');
197
+ assert.equal(parsed.meta.kbCatalog, 'not_created');
198
+ assert.deepEqual(JSON.parse(fs.readFileSync(output, 'utf8')), parsed.ok.config);
199
+ fs.rmSync(tempRoot, { recursive: true, force: true });
200
+ });
201
+
202
+ test('config create rejects missing new-format flags', () => {
203
+ const result = runCli(['config', 'create', '--config-scope', 'neutral', '--format', 'json']);
204
+ assert.equal(result.status, 2);
205
+ const parsed = JSON.parse(result.stdout);
206
+ assert.equal(parsed.error.code, 'usage_error');
207
+ assert.match(parsed.error.message, /--output/);
208
+ assert.match(parsed.error.message, /--worker/);
209
+ assert.match(parsed.error.message, /--gateway-mode/);
210
+ assert.match(parsed.error.message, /--resolution-policy/);
211
+ });
212
+
213
+ test('config create rejects KB in neutral runtime', () => {
214
+ const result = runCli([
215
+ 'config', 'create', '--config-scope', 'neutral', '--output', path.join(os.tmpdir(), 'should-not-write.json'),
216
+ '--gx', 'C:\\GeneXus18', '--worker', 'C:\\worker.exe', '--gateway-mode', 'stdio-isolated',
217
+ '--resolution-policy', 'strict', '--kb', 'C:\\KBs\\forbidden', '--format', 'json'
218
+ ]);
219
+ assert.equal(result.status, 2);
220
+ const parsed = JSON.parse(result.stdout);
221
+ assert.equal(parsed.error.code, 'usage_error');
222
+ assert.match(parsed.error.message, /--kb is not allowed/);
223
+ });
224
+
225
+ test('config create does not modify client registration', () => {
226
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-neutral-reg-'));
227
+ const output = path.join(tempRoot, 'runtime.json');
228
+ const marker = path.join(tempRoot, 'client-config.json');
229
+ fs.writeFileSync(marker, JSON.stringify({ mcpServers: { existing: { command: 'keep' } } }, null, 2));
230
+ const before = fs.readFileSync(marker, 'utf8');
231
+ const result = runCli([
232
+ 'config', 'create', '--config-scope', 'neutral', '--output', output,
233
+ '--gx', 'C:\\GeneXus18', '--worker', 'C:\\worker.exe', '--gateway-mode', 'stdio-isolated',
234
+ '--resolution-policy', 'strict', '--format', 'json'
235
+ ]);
236
+ assert.equal(result.status, 0);
237
+ assert.equal(fs.readFileSync(marker, 'utf8'), before);
238
+ assert.deepEqual(JSON.parse(result.stdout).ok.config, JSON.parse(fs.readFileSync(output, 'utf8')));
239
+ fs.rmSync(tempRoot, { recursive: true, force: true });
240
+ });
241
+ test('config migrate creates a neutral config with an atomic backup and read-back receipt', () => {
242
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-migrate-'));
243
+ try {
244
+ const source = path.join(tempRoot, 'legacy.json');
245
+ const target = path.join(tempRoot, 'neutral.json');
246
+ const legacy = {
247
+ GeneXus: { InstallationPath: 'C:\\GeneXus18', WorkerExecutable: 'C:\\worker.exe' },
248
+ Server: { HttpPort: 5000, McpStdio: true },
249
+ Environment: { KBPath: 'C:\\KBs\\main', DefaultKb: 'main', KBs: [{ Alias: 'main', Path: 'C:\\KBs\\main' }] }
250
+ };
251
+ fs.writeFileSync(source, JSON.stringify(legacy, null, 2));
252
+ const result = runCli(['config', 'migrate', '--from', source, '--output', target, '--format', 'json']);
253
+ assert.equal(result.status, 0);
254
+ const parsed = JSON.parse(result.stdout);
255
+ assert.equal(parsed.meta.command, 'config.migrate');
256
+ assert.equal(parsed.ok.readBack, true);
257
+ assert.ok(fs.existsSync(parsed.ok.backupPath));
258
+ assert.deepEqual(JSON.parse(fs.readFileSync(parsed.ok.backupPath, 'utf8')), legacy);
259
+ assert.equal(JSON.parse(fs.readFileSync(target, 'utf8')).ConfigSchemaVersion, 2);
260
+ assert.deepEqual(parsed.ok.notMigrated, ['Environment.KBPath', 'Environment.KBs', 'Environment.DefaultKb']);
261
+ } finally {
262
+ fs.rmSync(tempRoot, { recursive: true, force: true });
263
+ }
264
+ });
265
+
266
+ test('config migrate rolls back an existing destination when read-back fails', () => {
267
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-migrate-rollback-'));
268
+ try {
269
+ const source = path.join(tempRoot, 'legacy.json');
270
+ const target = path.join(tempRoot, 'neutral.json');
271
+ fs.writeFileSync(source, JSON.stringify({ GeneXus: { InstallationPath: 'C:\\GeneXus18' } }));
272
+ const original = JSON.stringify({ keep: 'old' }, null, 2);
273
+ fs.writeFileSync(target, original);
274
+ const result = runCli(['config', 'migrate', '--from', source, '--output', target, '--format', 'json'], {
275
+ env: { GENEXUS_MCP_MIGRATE_FAIL_READBACK: '1' }
276
+ });
277
+ assert.equal(result.status, 1);
278
+ const parsed = JSON.parse(result.stdout);
279
+ assert.equal(parsed.error.code, 'operation_error');
280
+ assert.equal(parsed.ok, undefined);
281
+ assert.equal(fs.readFileSync(target, 'utf8'), original);
282
+ assert.equal(parsed.error.rollback.rolledBack, true);
283
+ } finally {
284
+ fs.rmSync(tempRoot, { recursive: true, force: true });
285
+ }
286
+ });
287
+
288
+ test('config migrate rejects non-migratable fields when explicitly requested', () => {
289
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-migrate-reject-'));
290
+ try {
291
+ const source = path.join(tempRoot, 'legacy.json');
292
+ const target = path.join(tempRoot, 'neutral.json');
293
+ fs.writeFileSync(source, JSON.stringify({ Environment: { KBPath: 'C:\\KBs\\main' } }));
294
+ const result = runCli(['config', 'migrate', '--from', source, '--output', target, '--reject-non-migratable', '--format', 'json']);
295
+ assert.equal(result.status, 2);
296
+ const parsed = JSON.parse(result.stdout);
297
+ assert.match(parsed.error.message, /non-migratable/i);
298
+ assert.equal(fs.existsSync(target), false);
299
+ } finally {
300
+ fs.rmSync(tempRoot, { recursive: true, force: true });
301
+ }
302
+ });
303
+
180
304
  test('whoami without config returns disconnected state', () => {
181
305
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-test-'));
182
306
  const res = runCli(['whoami', '--format', 'json'], { cwd: tempRoot });
@@ -1084,7 +1208,7 @@ test('clients add patches OpenCode Desktop into shared opencode config', () => {
1084
1208
  assert.ok(fs.existsSync(opencodeCfg), 'shared opencode config should be created');
1085
1209
  const written = JSON.parse(fs.readFileSync(opencodeCfg, 'utf8'));
1086
1210
  assert.ok(written.mcp.genexus18mcp, 'shared config should contain genexus18mcp entry');
1087
- assert.equal(written.mcp.genexus18mcp.environment.GX_CONFIG_PATH, cfgPath);
1211
+ assert.equal(written.mcp.genexus18mcp.environment?.GX_CONFIG_PATH, undefined);
1088
1212
 
1089
1213
  const listRes = runCli(['clients', '--format', 'json'], { env });
1090
1214
  const row = JSON.parse(listRes.stdout).ok.clients.find((client) => client.id === 'opencode-desktop');
@@ -1302,7 +1426,7 @@ test('clients add preserves OpenCode 1.x direct mcp shape', () => {
1302
1426
  assert.equal(written.mcp.genexus18mcp.disabled, undefined);
1303
1427
  assert.ok(written.mcp.other, 'unrelated direct MCP server should be preserved');
1304
1428
  assert.equal(written.mcp.servers, undefined);
1305
- assert.deepEqual(written.mcp.genexus18mcp.environment, { GX_CONFIG_PATH: cfgPath });
1429
+ assert.equal(written.mcp.genexus18mcp.environment?.GX_CONFIG_PATH, undefined);
1306
1430
 
1307
1431
  const listed = runCli(['clients', '--format', 'json'], { env });
1308
1432
  assert.equal(listed.status, 0);
@@ -1333,7 +1457,7 @@ test('clients add preserves OpenCode v2 nested mcp.servers shape', () => {
1333
1457
  assert.equal(written.mcp.servers.genexus18mcp.enabled, undefined);
1334
1458
  assert.ok(written.mcp.servers.other, 'unrelated nested MCP server should be preserved');
1335
1459
  assert.equal(written.mcp.genexus18mcp, undefined);
1336
- assert.deepEqual(written.mcp.servers.genexus18mcp.environment, { GX_CONFIG_PATH: cfgPath });
1460
+ assert.equal(written.mcp.servers.genexus18mcp.environment?.GX_CONFIG_PATH, undefined);
1337
1461
 
1338
1462
  const listed = runCli(['clients', '--format', 'json'], { env });
1339
1463
  assert.equal(listed.status, 0);
@@ -1370,7 +1494,7 @@ test('init auto-registers detected OpenCode in either config layout', () => {
1370
1494
  const written = JSON.parse(fs.readFileSync(openCodeCfg, 'utf8'));
1371
1495
  const entry = label === 'nested' ? written.mcp.servers.genexus18mcp : written.mcp.genexus18mcp;
1372
1496
  assert.ok(entry, `${label} OpenCode entry should be present after init`);
1373
- assert.deepEqual(entry.environment, { GX_CONFIG_PATH: path.join(kbDir, 'config.json') });
1497
+ assert.equal(entry.environment?.GX_CONFIG_PATH, undefined);
1374
1498
  assert.ok(label === 'nested' ? written.mcp.servers.other : written.mcp.other);
1375
1499
  } finally {
1376
1500
  fs.rmSync(tempRoot, { recursive: true, force: true });
@@ -1378,6 +1502,52 @@ test('init auto-registers detected OpenCode in either config layout', () => {
1378
1502
  }
1379
1503
  });
1380
1504
 
1505
+ test('init with --global-config persists GX_CONFIG_PATH into client entry', () => {
1506
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-opencode-init-global-'));
1507
+ try {
1508
+ const env = sandboxHomeEnv(tempRoot);
1509
+ const kbDir = path.join(tempRoot, 'kb');
1510
+ const openCodeCfg = path.join(env.XDG_CONFIG_HOME, 'opencode', 'opencode.json');
1511
+ fs.mkdirSync(kbDir, { recursive: true });
1512
+ fs.mkdirSync(path.dirname(openCodeCfg), { recursive: true });
1513
+ fs.writeFileSync(openCodeCfg, JSON.stringify({ mcp: {} }, null, 2));
1514
+
1515
+ const result = runCli(
1516
+ ['init', '--kb', kbDir, '--gx', testGxPath, '--global-config', '--no-smoke', '--format', 'json'],
1517
+ { cwd: kbDir, env: { ...env, ...testGatewayEnv } }
1518
+ );
1519
+ assert.equal(result.status, 0, `init should succeed: ${result.stderr}`);
1520
+
1521
+ const written = JSON.parse(fs.readFileSync(openCodeCfg, 'utf8'));
1522
+ assert.ok(written.mcp.genexus18mcp);
1523
+ assert.equal(written.mcp.genexus18mcp.environment.GX_CONFIG_PATH, path.join(kbDir, 'config.json'));
1524
+ } finally {
1525
+ fs.rmSync(tempRoot, { recursive: true, force: true });
1526
+ }
1527
+ });
1528
+
1529
+ test('clients add with --global-config persists GX_CONFIG_PATH into client entry', () => {
1530
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-opencode-add-global-'));
1531
+ try {
1532
+ const env = sandboxHomeEnv(tempRoot);
1533
+ const cfgPath = path.join(tempRoot, 'config.json');
1534
+ const openCodeCfg = path.join(env.XDG_CONFIG_HOME, 'opencode', 'opencode.json');
1535
+ fs.mkdirSync(path.dirname(openCodeCfg), { recursive: true });
1536
+ fs.writeFileSync(cfgPath, JSON.stringify({ Environment: { KBPath: tempRoot } }));
1537
+ fs.writeFileSync(openCodeCfg, JSON.stringify({ mcp: {} }, null, 2));
1538
+
1539
+ const res = runCli(['clients', 'add', '--clients', 'opencode', '--global-config', '--format', 'json'], {
1540
+ env: { ...env, GX_CONFIG_PATH: cfgPath }
1541
+ });
1542
+ assert.equal(res.status, 0);
1543
+ const written = JSON.parse(fs.readFileSync(openCodeCfg, 'utf8'));
1544
+ assert.ok(written.mcp.genexus18mcp);
1545
+ assert.equal(written.mcp.genexus18mcp.environment.GX_CONFIG_PATH, cfgPath);
1546
+ } finally {
1547
+ fs.rmSync(tempRoot, { recursive: true, force: true });
1548
+ }
1549
+ });
1550
+
1381
1551
  test('init auto-registers detected OpenCode Desktop when its marker is present', () => {
1382
1552
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-opencode-desktop-init-'));
1383
1553
  try {
@@ -1835,3 +2005,93 @@ test('clients add rejects invalid --server-name characters with usage error', ()
1835
2005
  assert.equal(parsed.error.code, 'usage_error');
1836
2006
  assert.match(parsed.error.message, /alphanumeric/);
1837
2007
  });
2008
+
2009
+ function fsFailureProxy(realFs, failure) {
2010
+ let calls = 0;
2011
+ return new Proxy(realFs, {
2012
+ get(target, property) {
2013
+ if (property === failure.method) {
2014
+ return (...args) => {
2015
+ calls += 1;
2016
+ if (!failure.match || failure.match(args, calls)) throw new Error(failure.message);
2017
+ return target[property](...args);
2018
+ };
2019
+ }
2020
+ const value = target[property];
2021
+ return typeof value === 'function' ? value.bind(target) : value;
2022
+ }
2023
+ });
2024
+ }
2025
+
2026
+ function patchFailureFixture(prefix) {
2027
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
2028
+ const env = sandboxHomeEnv(tempRoot);
2029
+ process.env.XDG_CONFIG_HOME = env.XDG_CONFIG_HOME;
2030
+ process.env.APPDATA = env.APPDATA;
2031
+ const cfgPath = path.join(tempRoot, 'config.json');
2032
+ fs.writeFileSync(cfgPath, JSON.stringify({ Environment: { KBPath: tempRoot } }));
2033
+ const openCodeCfg = path.join(env.XDG_CONFIG_HOME, 'opencode', 'opencode.json');
2034
+ const vscodeCfg = path.join(env.APPDATA, 'Code', 'User', 'mcp.json');
2035
+ fs.mkdirSync(path.dirname(openCodeCfg), { recursive: true });
2036
+ fs.mkdirSync(path.dirname(vscodeCfg), { recursive: true });
2037
+ fs.writeFileSync(openCodeCfg, JSON.stringify({ mcp: { other: { type: 'local' } } }));
2038
+ fs.writeFileSync(vscodeCfg, JSON.stringify({ servers: { other: { type: 'stdio' } } }));
2039
+ return { tempRoot, env, cfgPath, openCodeCfg, vscodeCfg };
2040
+ }
2041
+
2042
+ test('patchClientConfig reports backup failure without claiming that client patched', () => {
2043
+ const fixture = patchFailureFixture('genexus-mcp-partial-backup-');
2044
+ try {
2045
+ const result = patchClientConfig(fixture.cfgPath, {
2046
+ ids: ['opencode', 'vscode'], onlyExisting: false,
2047
+ fs: fsFailureProxy(fs, { method: 'copyFileSync', message: 'backup denied', match: ([source]) => source === fixture.openCodeCfg })
2048
+ });
2049
+ assert.deepEqual(result.patched, ['VS Code']);
2050
+ assert.deepEqual(result.failed, [{ client: 'OpenCode (CLI)', reason: 'backup denied' }]);
2051
+ assert.equal(JSON.parse(fs.readFileSync(fixture.openCodeCfg, 'utf8')).mcp.genexus18mcp, undefined);
2052
+ assert.ok(JSON.parse(fs.readFileSync(fixture.vscodeCfg, 'utf8')).servers.genexus18mcp);
2053
+ } finally { fs.rmSync(fixture.tempRoot, { recursive: true, force: true }); }
2054
+ });
2055
+
2056
+ test('patchClientConfig preserves earlier success when a later client write fails', () => {
2057
+ const fixture = patchFailureFixture('genexus-mcp-partial-write-');
2058
+ try {
2059
+ const result = patchClientConfig(fixture.cfgPath, {
2060
+ ids: ['opencode', 'vscode'], onlyExisting: false,
2061
+ fs: fsFailureProxy(fs, { method: 'writeFileSync', message: 'write denied', match: ([filePath]) => filePath === `${fixture.vscodeCfg}.tmp-${process.pid}` })
2062
+ });
2063
+ assert.deepEqual(result.patched, ['OpenCode (CLI)']);
2064
+ assert.deepEqual(result.failed, [{ client: 'VS Code', reason: 'write denied' }]);
2065
+ assert.ok(JSON.parse(fs.readFileSync(fixture.openCodeCfg, 'utf8')).mcp.genexus18mcp);
2066
+ assert.equal(JSON.parse(fs.readFileSync(fixture.vscodeCfg, 'utf8')).servers.genexus18mcp, undefined);
2067
+ } finally { fs.rmSync(fixture.tempRoot, { recursive: true, force: true }); }
2068
+ });
2069
+
2070
+ test('patchClientConfig reports post-write read-back failure as partial state', () => {
2071
+ const fixture = patchFailureFixture('genexus-mcp-partial-readback-');
2072
+ try {
2073
+ const result = patchClientConfig(fixture.cfgPath, {
2074
+ ids: ['opencode', 'vscode'], onlyExisting: false,
2075
+ fs: fsFailureProxy(fs, { method: 'readFileSync', message: 'read-back denied', match: ([filePath], calls) => filePath === fixture.openCodeCfg && calls === 2 })
2076
+ });
2077
+ assert.deepEqual(result.patched, ['VS Code']);
2078
+ assert.deepEqual(result.failed, [{ client: 'OpenCode (CLI)', reason: 'post-write verification failed (genexus18mcp entry not found after write)' }]);
2079
+ assert.ok(JSON.parse(fs.readFileSync(fixture.openCodeCfg, 'utf8')).mcp.genexus18mcp);
2080
+ assert.ok(JSON.parse(fs.readFileSync(fixture.vscodeCfg, 'utf8')).servers.genexus18mcp);
2081
+ } finally { fs.rmSync(fixture.tempRoot, { recursive: true, force: true }); }
2082
+ });
2083
+
2084
+ test('patchClientConfig keeps a stale same-second backup and writes a distinct backup', () => {
2085
+ const fixture = patchFailureFixture('genexus-mcp-stale-backup-');
2086
+ try {
2087
+ const stamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
2088
+ const stale = `${fixture.openCodeCfg}.${stamp}.bak`;
2089
+ fs.writeFileSync(stale, 'stale backup');
2090
+ const result = patchClientConfig(fixture.cfgPath, { ids: ['opencode'], onlyExisting: false });
2091
+ assert.deepEqual(result.failed, []);
2092
+ assert.equal(fs.readFileSync(stale, 'utf8'), 'stale backup');
2093
+ const backups = fs.readdirSync(path.dirname(fixture.openCodeCfg)).filter((name) => name.startsWith(path.basename(fixture.openCodeCfg)) && name.endsWith('.bak'));
2094
+ assert.equal(backups.length, 2);
2095
+ assert.ok(backups.some((name) => name !== path.basename(stale)));
2096
+ } finally { fs.rmSync(fixture.tempRoot, { recursive: true, force: true }); }
2097
+ });
@@ -38,6 +38,15 @@ Entry points:
38
38
  - `genexus-mcp tools list`
39
39
  - `genexus-mcp config show`
40
40
 
41
+ Neutral installer/runtime setup:
42
+ - `genexus-mcp config create --config-scope neutral --output <path> --gx <path> --worker <path> --gateway-mode stdio-isolated --resolution-policy strict`
43
+ - `genexus-mcp clients add --all-clients` (or `--clients opencode,codex-cli,vscode,antigravity,gemini-cli`)
44
+ - `genexus-mcp config migrate --from <legacy.json> --output <neutral.json>` is the only configuration migration path. It copies runtime fields into the neutral schema (`ConfigSchemaVersion: 2`, `GatewayMode`, `GeneXus`, `Server`, and `Environment.ResolutionPolicy`), writes an atomic source backup, verifies the destination by read-back, and rolls back an existing destination if verification fails. The JSON receipt reports `backupPath`, `readBack`, `rolledBack`, `migrated`, and `notMigrated`.
45
+ - Legacy KB fields (`Environment.KBPath`, `KBs`, `DefaultKb`, `ActiveKb`) are deliberately not migrated because neutral configs are KB-free. Use `--reject-non-migratable` to fail before writing when any are present.
46
+ - `init` and `clients add` never migrate or rewrite an existing config automatically. `kb add`, `kb remove`, and `kb switch` remain the legacy catalog flow: they mutate the `Environment` catalog in the config selected by `GX_CONFIG_PATH` or the current directory. Neutral runtimes use explicit per-session MCP KB selection instead.
47
+
48
+ The neutral runtime contains no `KBPath`, `KBs`, `DefaultKb`, or `ActiveKb`; KB selection remains an explicit MCP session action. Adapters preserve unrelated servers and the existing OpenCode `mcp.<name>` versus `mcp.servers.<name>` layout. They add `GX_CONFIG_PATH` only when `--global-config` is explicitly requested.
49
+
41
50
  Rules:
42
51
  - Parse `stdout` only.
43
52
  - Expect envelope fields: `ok`, `error`, `help`, `meta`.
@@ -71,6 +80,36 @@ this file.
71
80
 
72
81
  For `tools/call`, parse `result.content[0].text` as JSON.
73
82
 
83
+ ### KB context, leases, and compatibility
84
+
85
+ Use the neutral `stdio-isolated` + `ResolutionPolicy: "strict"` configuration
86
+ for local work. Strict resolution is explicit `kb` → session `select` → strict
87
+ rules: persisted defaults do not silently select a session, and declared KBs
88
+ are never auto-opened. Set `ResolutionPolicy: "legacy"` only when preserving a
89
+ legacy client contract is intentional; it retains `config-default` →
90
+ `single-open` → `declared-first` fallback and legacy persistent `set_default`.
91
+
92
+ `genexus_kb action=open` opens/registers a worker and returns an owner-scoped
93
+ lease. `select`/`set_session_default` select only the current session and do
94
+ not persist config or grant a lease. `close` releases only the caller's lease.
95
+ In strict mode, a stateful KB-bound call without that lease returns
96
+ `KB_NOT_OWNED`, even if `kb` names the target; `kb` identifies a target but does
97
+ not transfer ownership. `KB_LEASE_INVALID` means an invalid or mismatched
98
+ token/context, and `KB_LEASE_EXPIRED` means the lease TTL elapsed. A duplicate
99
+ worker lock is reported as `KB_LOCKED`; do not blindly retry or expect proxy
100
+ fallback. Sessionless HTTP cannot use `select` and returns
101
+ `KB_SESSION_UNAVAILABLE`.
102
+
103
+ The default local-friendly posture accepts an explicit valid local KB path.
104
+ Hardened deployments add OS/root/network controls and must expose that posture
105
+ in diagnostics; `HttpPort=0` is not proof of isolation.
106
+
107
+ For HTTP, `GXMCP_HTTP_TOKEN` is an environment secret. Once set, send it on
108
+ every `/mcp` request as `Authorization: Bearer <token>` or `X-GXMCP-Token`.
109
+ Without a token, loopback remains available; non-loopback requests are refused.
110
+ Never place the token in client registration, config examples, MCP payloads, or
111
+ logs. Restart after rotating it.
112
+
74
113
  Gateway AXI-like enrichments are additive (under `_meta` — underscore-prefixed per MCP convention):
75
114
  - `_meta.schemaVersion = mcp-axi/2` (v2.0.0+)
76
115
  - `_meta.tool = <tool-name>`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genexus-mcp",
3
- "version": "3.2.1",
3
+ "version": "3.2.2",
4
4
  "mcpName": "io.github.lennix1337/genexus",
5
5
  "description": "GeneXus 17, GeneXus 18 MCP server — read, edit, and analyze Knowledge Base objects directly from Claude, Cursor, and other AI agents over the Model Context Protocol.",
6
6
  "keywords": [
@@ -32,7 +32,7 @@
32
32
  "automation"
33
33
  ],
34
34
  "scripts": {
35
- "test": "node --test cli/run.test.js cli/lib/client-adapters.test.js",
35
+ "test": "node --test cli/run.test.js cli/lib/client-adapters.test.js cli/docs.test.js",
36
36
  "lint": "eslint cli scripts eslint.config.js --max-warnings=0",
37
37
  "test:one": "node scripts/test-one.js",
38
38
  "prepack": "node scripts/clean-package.js",
@@ -7,7 +7,7 @@
7
7
  "targets": {
8
8
  ".NETCoreApp,Version=v10.0": {},
9
9
  ".NETCoreApp,Version=v10.0/win-x64": {
10
- "GxMcp.Gateway/3.2.1": {
10
+ "GxMcp.Gateway/3.2.2": {
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/3.2.1": {
69
+ "GxMcp.Gateway/3.2.2": {
70
70
  "type": "project",
71
71
  "serviceable": false,
72
72
  "sha512": ""
Binary file
Binary file
@@ -1,17 +1,19 @@
1
1
  {
2
+ "Environment": {
3
+ "ResolutionPolicy": "strict"
4
+ },
5
+ "ConfigSchemaVersion": 2,
2
6
  "Server": {
7
+ "HttpPort": 0,
8
+ "WorkerIdleTimeoutMinutes": 5,
3
9
  "McpStdio": true,
4
- "HttpPort": 5000
5
- },
6
- "Logging": {
7
- "Level": "Debug",
8
- "Path": "logs"
9
- },
10
- "Environment": {
11
- "KBPath": "C:\\\\KBs\\\\YourKB"
10
+ "TerseResponses": true,
11
+ "SessionIdleTimeoutMinutes": 10,
12
+ "EmitStructuredContent": false
12
13
  },
14
+ "GatewayMode": "stdio-isolated",
13
15
  "GeneXus": {
14
- "WorkerExecutable": "worker\\\\GxMcp.Worker.exe",
15
- "InstallationPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus18"
16
+ "InstallationPath": "C:\\Program Files (x86)\\GeneXus\\GeneXus18",
17
+ "WorkerExecutable": "worker\\\\GxMcp.Worker.exe"
16
18
  }
17
19
  }
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": "gxmcp-release-manifest/1",
3
- "version": "3.2.1",
4
- "sourceCommit": "4be312bd5d3269735d75b2755f4d4ae31699e62a",
3
+ "version": "3.2.2",
4
+ "sourceCommit": "3b9e16fe449c6ae0d3555112cd433c3ecb358934",
5
5
  "sourceCommitPolicy": "exact-tag",
6
- "generatedAtUtc": "2026-09-10T00:11:21.1284622Z",
6
+ "generatedAtUtc": "2026-09-10T17:38:42.0225221Z",
7
7
  "runtime": {
8
8
  "gateway": "net10.0-windows",
9
9
  "worker": "net48-x86",
@@ -15,33 +15,33 @@
15
15
  "2026-07-28"
16
16
  ],
17
17
  "schema": "tool_definitions.json",
18
- "schemaSha256": "0720f2fa24d927e70f8d7520783de97f6671cb6b0ce9e6e8f02e74520dcc180b",
18
+ "schemaSha256": "a548b6b8a4a8a88270b320e669e60755c61169e5d771591e92b21edaa5c6afad",
19
19
  "provenance": "gxmcp-sbom.json",
20
20
  "artifacts": [
21
21
  {
22
22
  "path": "gxmcp-sbom.json",
23
23
  "size": 590,
24
- "sha256": "d15c96e6648553224eff033fc8b79ab2181cf35d4f8300eb64481ce9e5493e38"
24
+ "sha256": "f815950ad437f1949fc23ba06dfa45df814172070f0f94e4f621635d5e31026d"
25
25
  },
26
26
  {
27
27
  "path": "GxMcp.Gateway.exe",
28
28
  "size": 162304,
29
- "sha256": "11087b4597044123f96f1a06c8d3107e8d60f461953bc172ee5cd1dd78ea44e5"
29
+ "sha256": "bc99684803a349f2ca3343ccf330d637bc8fe72263217e7596139a5361488048"
30
30
  },
31
31
  {
32
32
  "path": "nexus-ide.vsix",
33
- "size": 1318667,
34
- "sha256": "929d9f32f9059250b28d3638b49f2bb0648f94c9b1276aad9252019948a3a0bd"
33
+ "size": 1318670,
34
+ "sha256": "b8adec54e5735f7df64146afae3c86c4bd001e7b2bab39ea17797ec1c36335a1"
35
35
  },
36
36
  {
37
37
  "path": "tool_definitions.json",
38
- "size": 106778,
39
- "sha256": "0720f2fa24d927e70f8d7520783de97f6671cb6b0ce9e6e8f02e74520dcc180b"
38
+ "size": 109656,
39
+ "sha256": "a548b6b8a4a8a88270b320e669e60755c61169e5d771591e92b21edaa5c6afad"
40
40
  },
41
41
  {
42
42
  "path": "worker/GxMcp.Worker.exe",
43
- "size": 2934784,
44
- "sha256": "7b4c84a0377c7023017e8d04d0a6b33caa9f3a88728e134a4dba1834eb295b92"
43
+ "size": 2989568,
44
+ "sha256": "9f3e594e79c09672f084d6d3a7ac7b07ca63e5e20de7fa5f2d65e00ed0c3b3dd"
45
45
  }
46
46
  ]
47
47
  }
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "schemaVersion": "gxmcp-provenance/1",
3
- "version": "3.2.1",
4
- "sourceCommit": "4be312bd5d3269735d75b2755f4d4ae31699e62a",
3
+ "version": "3.2.2",
4
+ "sourceCommit": "3b9e16fe449c6ae0d3555112cd433c3ecb358934",
5
5
  "sourceCommitPolicy": "exact-tag",
6
6
  "components": [
7
7
  {
8
8
  "name": "genexus-mcp",
9
9
  "lockfile": "package-lock.json",
10
10
  "size": 39914,
11
- "sha256": "9833acd17a01c6e279779e69a5395569b696b4c424adf94f41ecaf53ffdef442"
11
+ "sha256": "28352a3b59b5d25e58f7150e54ed003f813cfa0b68b7123a6e452ca21dd6a4a8"
12
12
  },
13
13
  {
14
14
  "name": "nexus-ide",
15
15
  "lockfile": "src/nexus-ide/package-lock.json",
16
16
  "size": 274983,
17
- "sha256": "b9acea1ca9a3e8b95b0b57812b77cd2e0dfb33c8b5150bae7a852300a16d5603"
17
+ "sha256": "21f8d8457e5ae77c2ab3e3670dda9b89f792d741c31a05367bdbbb8e8b6d1d1b"
18
18
  }
19
19
  ]
20
20
  }
Binary file