genexus-mcp 1.3.0 → 2.0.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.
- package/README.md +13 -5
- package/cli/commands/axi.js +5 -1
- package/cli/index.js +10 -1
- package/cli/lib/update-check.js +214 -0
- package/cli/run.test.js +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,13 +103,17 @@ Notes:
|
|
|
103
103
|
## 🛠️ Tool Surface (Skills)
|
|
104
104
|
*(See `GEMINI.md` for extended guidelines).* The worker natively exposes the following tools to the MCP Router:
|
|
105
105
|
|
|
106
|
-
- **Search & Discovery**: `genexus_query`, `genexus_read`, `
|
|
107
|
-
- **Editing & Architecture**: `genexus_edit`, `
|
|
106
|
+
- **Search & Discovery**: `genexus_query`, `genexus_read`, `genexus_inspect`, `genexus_list_objects`, `genexus_properties`
|
|
107
|
+
- **Editing & Architecture**: `genexus_edit`, `genexus_create_object`, `genexus_refactor`, `genexus_forge`
|
|
108
108
|
- **Analysis:** `genexus_analyze`, `genexus_inject_context`, `genexus_doc`, `genexus_explain_code`, `genexus_summarize`
|
|
109
109
|
- **File System & Assets**: `genexus_asset`, `genexus_export_object`, `genexus_import_object`
|
|
110
110
|
- **History & DB**: `genexus_history`, `genexus_get_sql`, `genexus_structure`
|
|
111
111
|
- **Native Layout SDK**: `genexus_layout` (`get_tree`, `find_controls`, `set_property`, `set_properties`, `rename_printblock`, `add_printblock`, `get_preview`, `scan_mutators`)
|
|
112
112
|
|
|
113
|
+
> **`genexus_edit` modes:** `xml` (default — full XML replacement), `ops` (typed semantic op catalog: `set_attribute`, `add_attribute`, `remove_attribute`, `add_rule`, `remove_rule`, `set_property`), `patch` (JSON-Patch RFC 6902 array over canonical JSON object; legacy string-form patch also accepted for backward compatibility).
|
|
114
|
+
>
|
|
115
|
+
> **All write tools** accept `dryRun: true` (returns a preview `plan` envelope without mutating the KB) and `idempotencyKey` (safe retries — concurrent calls with same key are coalesced; results cached for 15 min by default).
|
|
116
|
+
|
|
113
117
|
Layout color note:
|
|
114
118
|
- For `ForeColor`, `BackColor`, `BorderColor`, send color values as palette names (`Black`, `Blue`, `Red`, `Transparent`) or RGB token (`R; G; B|`) to avoid nested SDK wrappers.
|
|
115
119
|
- **Lifecycle & Build**: `genexus_lifecycle`, `genexus_test`, `genexus_format`
|
|
@@ -119,10 +123,14 @@ Layout color note:
|
|
|
119
123
|
|
|
120
124
|
For `tools/call`, the gateway keeps MCP compatibility and adds lightweight response metadata:
|
|
121
125
|
|
|
122
|
-
- `
|
|
123
|
-
- `
|
|
126
|
+
- `_meta.schemaVersion` currently `mcp-axi/2`
|
|
127
|
+
- `_meta.tool` with the normalized tool name
|
|
124
128
|
- collection helpers such as `returned`, `total`, `empty`, and (when inferable) `hasMore` and `nextOffset`
|
|
125
|
-
- truncation signals via `
|
|
129
|
+
- truncation signals via `_meta.truncated` plus contextual `help`
|
|
130
|
+
- `_meta.idempotent: true` on cache-hit responses
|
|
131
|
+
- `_meta.batched: true` on `targets[]` multi-object responses
|
|
132
|
+
- `_meta.dryRun: true` on dry-run preview responses
|
|
133
|
+
- `_meta.removedTools` advertised on `initialize` for proactive agent detection of removed tools
|
|
126
134
|
|
|
127
135
|
For list-heavy calls (`genexus_query`, `genexus_list_objects`), optional arguments can reduce token usage:
|
|
128
136
|
|
package/cli/commands/axi.js
CHANGED
|
@@ -871,6 +871,10 @@ function commandHelpMap() {
|
|
|
871
871
|
usage: 'genexus-mcp llm help [--full] [--fields f1,f2] [--format toon|json|text]',
|
|
872
872
|
examples: ['genexus-mcp llm help --format json', 'genexus-mcp llm help --full --format json']
|
|
873
873
|
},
|
|
874
|
+
update: {
|
|
875
|
+
usage: 'genexus-mcp update [--format toon|json|text]',
|
|
876
|
+
examples: ['genexus-mcp update', 'genexus-mcp update --format json']
|
|
877
|
+
},
|
|
874
878
|
layout: {
|
|
875
879
|
usage: 'genexus-mcp layout status [--title "GeneXus"] [--format ...] OR genexus-mcp layout run --action <focus|activate-layout|activate-tab|send-keys|type-text|click> [--tab "Layout"] [--keys "..."] [--text "..."] [--x N --y N] [--title "..."] [--format ...] OR genexus-mcp layout inspect [--tab "Layout"] [--limit N] [--full] [--title "..."] [--format ...]',
|
|
876
880
|
examples: ['genexus-mcp layout status --format json', 'genexus-mcp layout run --action activate-tab --tab "Layout" --format json', 'genexus-mcp layout inspect --tab Layout --format json']
|
|
@@ -935,7 +939,7 @@ async function handleHelp(targetCommand, ctx) {
|
|
|
935
939
|
bin: binPath,
|
|
936
940
|
command: 'genexus-mcp',
|
|
937
941
|
description: 'GeneXus MCP launcher and AXI-oriented utility CLI',
|
|
938
|
-
commands: ['home', 'axi home', 'status', 'doctor', 'tools list', 'config show', 'layout status', 'layout run', 'layout inspect', 'init', 'llm help', 'help'],
|
|
942
|
+
commands: ['home', 'axi home', 'status', 'doctor', 'tools list', 'config show', 'layout status', 'layout run', 'layout inspect', 'init', 'llm help', 'update', 'help'],
|
|
939
943
|
defaults: { format: 'toon', limit: 100 }
|
|
940
944
|
},
|
|
941
945
|
help: [
|
package/cli/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
usageEnvelope,
|
|
23
23
|
commandHelpMap
|
|
24
24
|
} = require('./commands/axi');
|
|
25
|
+
const { startBackgroundUpdateCheck, handleUpdate } = require('./lib/update-check');
|
|
25
26
|
|
|
26
27
|
const EXIT_CODES = {
|
|
27
28
|
OK: 0,
|
|
@@ -43,7 +44,7 @@ const GLOBAL_DEFAULTS = {
|
|
|
43
44
|
help: false
|
|
44
45
|
};
|
|
45
46
|
|
|
46
|
-
const KNOWN_COMMANDS = new Set(['status', 'doctor', 'tools', 'config', 'init', 'setup', 'help', 'home', 'axi', 'llm', 'layout']);
|
|
47
|
+
const KNOWN_COMMANDS = new Set(['status', 'doctor', 'tools', 'config', 'init', 'setup', 'help', 'home', 'axi', 'llm', 'layout', 'update']);
|
|
47
48
|
|
|
48
49
|
function parseArgs(argv) {
|
|
49
50
|
const result = {
|
|
@@ -332,12 +333,17 @@ function resolveMetaCommand(parsed, targetHelp) {
|
|
|
332
333
|
if (parsed.subcommand === 'inspect') return 'layout.inspect';
|
|
333
334
|
return 'layout.status';
|
|
334
335
|
}
|
|
336
|
+
if (parsed.command === 'update') return 'update';
|
|
335
337
|
return parsed.command || 'unknown';
|
|
336
338
|
}
|
|
337
339
|
|
|
338
340
|
async function main(argv) {
|
|
339
341
|
const parsed = parseArgs(argv);
|
|
340
342
|
|
|
343
|
+
if (parsed.command !== 'update') {
|
|
344
|
+
startBackgroundUpdateCheck({ quiet: parsed.options.quiet });
|
|
345
|
+
}
|
|
346
|
+
|
|
341
347
|
if (!parsed.command) {
|
|
342
348
|
return launchGateway(argv, parsed.options);
|
|
343
349
|
}
|
|
@@ -438,6 +444,9 @@ async function main(argv) {
|
|
|
438
444
|
case 'init':
|
|
439
445
|
result = await handleInit(parsed.options, ctx);
|
|
440
446
|
break;
|
|
447
|
+
case 'update':
|
|
448
|
+
result = await handleUpdate(parsed.options, ctx);
|
|
449
|
+
break;
|
|
441
450
|
default:
|
|
442
451
|
writeStructured(
|
|
443
452
|
process.stdout,
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
|
|
6
|
+
const REPO = 'lennix1337/Genexus18MCP';
|
|
7
|
+
const NPM_PACKAGE = 'genexus-mcp';
|
|
8
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
const FETCH_TIMEOUT_MS = 2000;
|
|
10
|
+
|
|
11
|
+
function getPackageVersion() {
|
|
12
|
+
try {
|
|
13
|
+
const pkg = require('../../package.json');
|
|
14
|
+
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getCacheFile() {
|
|
21
|
+
return path.join(os.homedir(), '.genexus-mcp', 'update-check.json');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readCache() {
|
|
25
|
+
try {
|
|
26
|
+
const raw = fs.readFileSync(getCacheFile(), 'utf8');
|
|
27
|
+
const data = JSON.parse(raw);
|
|
28
|
+
if (data && typeof data === 'object') return data;
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeCache(data) {
|
|
35
|
+
try {
|
|
36
|
+
const file = getCacheFile();
|
|
37
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
38
|
+
fs.writeFileSync(file, JSON.stringify(data), 'utf8');
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stripV(v) {
|
|
44
|
+
return typeof v === 'string' ? v.replace(/^v/i, '').trim() : '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseSemver(v) {
|
|
48
|
+
const s = stripV(v);
|
|
49
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(s);
|
|
50
|
+
if (!m) return null;
|
|
51
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function compareSemver(a, b) {
|
|
55
|
+
const pa = parseSemver(a);
|
|
56
|
+
const pb = parseSemver(b);
|
|
57
|
+
if (!pa || !pb) return 0;
|
|
58
|
+
for (let i = 0; i < 3; i += 1) {
|
|
59
|
+
if (pa[i] > pb[i]) return 1;
|
|
60
|
+
if (pa[i] < pb[i]) return -1;
|
|
61
|
+
}
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fetchLatestRelease() {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const options = {
|
|
68
|
+
hostname: 'api.github.com',
|
|
69
|
+
path: `/repos/${REPO}/releases/latest`,
|
|
70
|
+
method: 'GET',
|
|
71
|
+
headers: {
|
|
72
|
+
'User-Agent': `${NPM_PACKAGE}-cli`,
|
|
73
|
+
'Accept': 'application/vnd.github+json'
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const req = https.request(options, (res) => {
|
|
78
|
+
if (res.statusCode !== 200) {
|
|
79
|
+
res.resume();
|
|
80
|
+
resolve(null);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let body = '';
|
|
84
|
+
res.setEncoding('utf8');
|
|
85
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
86
|
+
res.on('end', () => {
|
|
87
|
+
try {
|
|
88
|
+
const json = JSON.parse(body);
|
|
89
|
+
const tag = stripV(json.tag_name || '');
|
|
90
|
+
const url = typeof json.html_url === 'string' ? json.html_url : null;
|
|
91
|
+
if (!tag) { resolve(null); return; }
|
|
92
|
+
resolve({ latestVersion: tag, releaseUrl: url });
|
|
93
|
+
} catch {
|
|
94
|
+
resolve(null);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
req.on('error', () => resolve(null));
|
|
100
|
+
req.setTimeout(FETCH_TIMEOUT_MS, () => {
|
|
101
|
+
req.destroy();
|
|
102
|
+
resolve(null);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
req.end();
|
|
106
|
+
if (typeof req.unref === 'function') req.unref();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatBanner(current, latest, releaseUrl) {
|
|
111
|
+
const lines = [
|
|
112
|
+
`[genexus-mcp] update available: v${current} -> v${latest}`,
|
|
113
|
+
`[genexus-mcp] run: npm install -g ${NPM_PACKAGE}@latest`
|
|
114
|
+
];
|
|
115
|
+
if (releaseUrl) lines.push(`[genexus-mcp] release: ${releaseUrl}`);
|
|
116
|
+
return lines.join('\n') + '\n';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isDisabled(opts) {
|
|
120
|
+
if (process.env.GENEXUS_MCP_NO_UPDATE_CHECK === '1') return true;
|
|
121
|
+
if (opts && opts.quiet) return true;
|
|
122
|
+
if (!process.stderr || !process.stderr.isTTY) return true;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function maybePrintCachedBanner(opts) {
|
|
127
|
+
const current = getPackageVersion();
|
|
128
|
+
if (!current) return;
|
|
129
|
+
const cache = readCache();
|
|
130
|
+
if (!cache || !cache.latestVersion) return;
|
|
131
|
+
if (compareSemver(cache.latestVersion, current) > 0) {
|
|
132
|
+
try {
|
|
133
|
+
process.stderr.write(formatBanner(current, cache.latestVersion, cache.releaseUrl || null));
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function scheduleBackgroundFetch() {
|
|
140
|
+
const cache = readCache();
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
if (cache && typeof cache.checkedAt === 'number' && (now - cache.checkedAt) < CACHE_TTL_MS) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fetchLatestRelease().then((result) => {
|
|
147
|
+
if (!result) return;
|
|
148
|
+
writeCache({
|
|
149
|
+
checkedAt: Date.now(),
|
|
150
|
+
latestVersion: result.latestVersion,
|
|
151
|
+
releaseUrl: result.releaseUrl
|
|
152
|
+
});
|
|
153
|
+
}).catch(() => {});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function startBackgroundUpdateCheck(opts) {
|
|
157
|
+
if (isDisabled(opts)) return;
|
|
158
|
+
maybePrintCachedBanner(opts);
|
|
159
|
+
scheduleBackgroundFetch();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function handleUpdate(_options, ctx) {
|
|
163
|
+
const current = getPackageVersion();
|
|
164
|
+
const result = await fetchLatestRelease();
|
|
165
|
+
|
|
166
|
+
if (!result) {
|
|
167
|
+
return {
|
|
168
|
+
exitCode: ctx.EXIT_CODES.OK,
|
|
169
|
+
envelope: {
|
|
170
|
+
ok: {
|
|
171
|
+
current,
|
|
172
|
+
latest: null,
|
|
173
|
+
updateAvailable: false,
|
|
174
|
+
fetched: false
|
|
175
|
+
},
|
|
176
|
+
help: ['Could not reach GitHub releases API. Check connectivity or retry later.']
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
writeCache({
|
|
182
|
+
checkedAt: Date.now(),
|
|
183
|
+
latestVersion: result.latestVersion,
|
|
184
|
+
releaseUrl: result.releaseUrl
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const updateAvailable = compareSemver(result.latestVersion, current || '0.0.0') > 0;
|
|
188
|
+
const help = updateAvailable
|
|
189
|
+
? [`Run: npm install -g ${NPM_PACKAGE}@latest`, result.releaseUrl ? `Release: ${result.releaseUrl}` : null].filter(Boolean)
|
|
190
|
+
: ['Already on latest version.'];
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
exitCode: ctx.EXIT_CODES.OK,
|
|
194
|
+
envelope: {
|
|
195
|
+
ok: {
|
|
196
|
+
current,
|
|
197
|
+
latest: result.latestVersion,
|
|
198
|
+
releaseUrl: result.releaseUrl,
|
|
199
|
+
updateAvailable,
|
|
200
|
+
installCommand: `npm install -g ${NPM_PACKAGE}@latest`,
|
|
201
|
+
fetched: true
|
|
202
|
+
},
|
|
203
|
+
help
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
startBackgroundUpdateCheck,
|
|
210
|
+
handleUpdate,
|
|
211
|
+
compareSemver,
|
|
212
|
+
parseSemver,
|
|
213
|
+
getPackageVersion
|
|
214
|
+
};
|
package/cli/run.test.js
CHANGED
|
@@ -5,6 +5,7 @@ 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
9
|
|
|
9
10
|
const cliPath = path.join(__dirname, 'run.js');
|
|
10
11
|
|
|
@@ -304,6 +305,24 @@ test('quiet flag suppresses launcher stderr noise', () => {
|
|
|
304
305
|
assert.equal(result.stderr.trim(), '');
|
|
305
306
|
});
|
|
306
307
|
|
|
308
|
+
test('update --help returns usage entry', () => {
|
|
309
|
+
const result = runCli(['update', '--help', '--format', 'json']);
|
|
310
|
+
assert.equal(result.status, 0);
|
|
311
|
+
|
|
312
|
+
const parsed = JSON.parse(result.stdout);
|
|
313
|
+
assert.equal(parsed.meta.command, 'help');
|
|
314
|
+
assert.equal(parsed.ok.command, 'update');
|
|
315
|
+
assert.ok(parsed.ok.usage.includes('genexus-mcp update'));
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('compareSemver detects newer, older, equal versions', () => {
|
|
319
|
+
assert.equal(compareSemver('1.3.1', '1.3.0'), 1);
|
|
320
|
+
assert.equal(compareSemver('v1.4.0', '1.3.9'), 1);
|
|
321
|
+
assert.equal(compareSemver('1.3.0', '1.3.0'), 0);
|
|
322
|
+
assert.equal(compareSemver('1.2.9', '1.3.0'), -1);
|
|
323
|
+
assert.equal(compareSemver('garbage', '1.0.0'), 0);
|
|
324
|
+
});
|
|
325
|
+
|
|
307
326
|
test('gateway passthrough remains intact when no AXI subcommand is used', () => {
|
|
308
327
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-test-'));
|
|
309
328
|
const fakeGateway = path.join(tempRoot, 'fake-gateway.js');
|