genexus-mcp 3.2.0 → 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/README.md +7 -6
- package/cli/commands/axi.js +116 -7
- package/cli/docs.test.js +29 -0
- package/cli/index.js +57 -6
- package/cli/lib/client-adapters.test.js +183 -3
- package/cli/lib/config.js +203 -61
- package/cli/run.test.js +265 -5
- package/docs/llm_cli_mcp_playbook.md +39 -0
- package/package.json +2 -2
- 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 +12 -10
- package/publish/gxmcp-manifest.json +12 -12
- package/publish/gxmcp-sbom.json +4 -4
- package/publish/nexus-ide.vsix +0 -0
- package/publish/tool_definitions.json +9 -5
- package/publish/worker/GxMcp.Worker.exe +0 -0
- package/publish/worker/sdk-compatibility.json +24 -0
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
69
|
+
"GxMcp.Gateway/3.2.2": {
|
|
70
70
|
"type": "project",
|
|
71
71
|
"serviceable": false,
|
|
72
72
|
"sha512": ""
|
|
Binary file
|
|
Binary file
|
package/publish/config.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
"WorkerExecutable": "worker\\\\GxMcp.Worker.exe"
|
|
2
|
+
"Environment": {
|
|
3
|
+
"ResolutionPolicy": "strict"
|
|
5
4
|
},
|
|
5
|
+
"ConfigSchemaVersion": 2,
|
|
6
6
|
"Server": {
|
|
7
|
+
"HttpPort": 0,
|
|
8
|
+
"WorkerIdleTimeoutMinutes": 5,
|
|
7
9
|
"McpStdio": true,
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"Level": "Debug",
|
|
12
|
-
"Path": "logs"
|
|
10
|
+
"TerseResponses": true,
|
|
11
|
+
"SessionIdleTimeoutMinutes": 10,
|
|
12
|
+
"EmitStructuredContent": false
|
|
13
13
|
},
|
|
14
|
-
"
|
|
15
|
-
|
|
14
|
+
"GatewayMode": "stdio-isolated",
|
|
15
|
+
"GeneXus": {
|
|
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.
|
|
4
|
-
"sourceCommit": "
|
|
3
|
+
"version": "3.2.2",
|
|
4
|
+
"sourceCommit": "3b9e16fe449c6ae0d3555112cd433c3ecb358934",
|
|
5
5
|
"sourceCommitPolicy": "exact-tag",
|
|
6
|
-
"generatedAtUtc": "2026-09-
|
|
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": "
|
|
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": "
|
|
24
|
+
"sha256": "f815950ad437f1949fc23ba06dfa45df814172070f0f94e4f621635d5e31026d"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"path": "GxMcp.Gateway.exe",
|
|
28
28
|
"size": 162304,
|
|
29
|
-
"sha256": "
|
|
29
|
+
"sha256": "bc99684803a349f2ca3343ccf330d637bc8fe72263217e7596139a5361488048"
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
"path": "nexus-ide.vsix",
|
|
33
|
-
"size":
|
|
34
|
-
"sha256": "
|
|
33
|
+
"size": 1318670,
|
|
34
|
+
"sha256": "b8adec54e5735f7df64146afae3c86c4bd001e7b2bab39ea17797ec1c36335a1"
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"path": "tool_definitions.json",
|
|
38
|
-
"size":
|
|
39
|
-
"sha256": "
|
|
38
|
+
"size": 109656,
|
|
39
|
+
"sha256": "a548b6b8a4a8a88270b320e669e60755c61169e5d771591e92b21edaa5c6afad"
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
"path": "worker/GxMcp.Worker.exe",
|
|
43
|
-
"size":
|
|
44
|
-
"sha256": "
|
|
43
|
+
"size": 2989568,
|
|
44
|
+
"sha256": "9f3e594e79c09672f084d6d3a7ac7b07ca63e5e20de7fa5f2d65e00ed0c3b3dd"
|
|
45
45
|
}
|
|
46
46
|
]
|
|
47
47
|
}
|
package/publish/gxmcp-sbom.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "gxmcp-provenance/1",
|
|
3
|
-
"version": "3.2.
|
|
4
|
-
"sourceCommit": "
|
|
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": "
|
|
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": "
|
|
17
|
+
"sha256": "21f8d8457e5ae77c2ab3e3670dda9b89f792d741c31a05367bdbbb8e8b6d1d1b"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|
package/publish/nexus-ide.vsix
CHANGED
|
Binary file
|