@biznetgio/neo-app 0.4.34 → 0.4.36
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/package.json +1 -1
- package/src/mcp/tools.js +72 -14
package/package.json
CHANGED
package/src/mcp/tools.js
CHANGED
|
@@ -40,6 +40,26 @@ function requireEnv(env) {
|
|
|
40
40
|
return e;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// deleteAndConverge deletes a resource, then polls its endpoint until the inventory
|
|
44
|
+
// actually converges (GET → 404), so a delete isn't reported done while the record
|
|
45
|
+
// still exists. Returns a durable operation reference (`del-<id>`) + convergence
|
|
46
|
+
// state (handoff 3.6; 0.4.34 retest §9 "every delete returns a durable operation ID").
|
|
47
|
+
async function deleteAndConverge(api, path, resourceId, kind) {
|
|
48
|
+
const { status, data } = await api.del(path);
|
|
49
|
+
let converged = false, checks = 0;
|
|
50
|
+
for (let i = 0; i < 12; i++) {
|
|
51
|
+
checks++;
|
|
52
|
+
try {
|
|
53
|
+
await api.get(path);
|
|
54
|
+
await new Promise((r) => setTimeout(r, 500)); // still present — keep polling
|
|
55
|
+
} catch (e) {
|
|
56
|
+
if (e && e.status === 404) { converged = true; break; }
|
|
57
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { operationId: 'del-' + resourceId, kind, status: (data && data.status) || 'deleted', converged, convergenceChecks: checks };
|
|
61
|
+
}
|
|
62
|
+
|
|
43
63
|
// gbFromSize parses "10Gi"/"10G"/"10" → 10 (integer GB), else undefined.
|
|
44
64
|
function gbFromSize(v) {
|
|
45
65
|
if (v == null) return undefined;
|
|
@@ -313,7 +333,7 @@ export const TOOLS = [
|
|
|
313
333
|
},
|
|
314
334
|
{
|
|
315
335
|
name: 'neo_scale_service',
|
|
316
|
-
description: 'Scale a service to N replicas. May return pending_approval under org policy.',
|
|
336
|
+
description: 'Scale a service to N replicas. Scaling changes cost, so without confirm:true it returns the new estimated cost and scales nothing (re-call with confirm:true). May return pending_approval under org policy.',
|
|
317
337
|
mutating: true,
|
|
318
338
|
schema: {
|
|
319
339
|
type: 'object',
|
|
@@ -321,10 +341,22 @@ export const TOOLS = [
|
|
|
321
341
|
projectId: str('Project id'),
|
|
322
342
|
service: str('Service/app name'),
|
|
323
343
|
replicas: int('Desired replica count (>=0)'),
|
|
344
|
+
confirm: { type: 'boolean', description: 'Approve the cost change: without confirm:true the tool returns the new cost and scales nothing.' },
|
|
324
345
|
},
|
|
325
346
|
required: ['projectId', 'service', 'replicas'],
|
|
326
347
|
},
|
|
327
348
|
async call(api, args) {
|
|
349
|
+
if (args.confirm !== true) {
|
|
350
|
+
let computeTier;
|
|
351
|
+
try {
|
|
352
|
+
const apps = (await api.get(`/api/v1/projects/${args.projectId}/apps`)).data;
|
|
353
|
+
const list = Array.isArray(apps) ? apps : (apps.applications || []);
|
|
354
|
+
const svc = list.find((a) => a.name === args.service || a.id === args.service);
|
|
355
|
+
computeTier = svc && (svc.cpuLimit || svc.computeTier);
|
|
356
|
+
} catch { /* best effort — gate regardless */ }
|
|
357
|
+
const gate = await confirmCost(api, args, { computeTier, replicas: args.replicas });
|
|
358
|
+
if (gate) return { ...gate, change: `scale "${args.service}" to ${args.replicas} replica(s)`, message: 'Scaling changes cost — show the user and re-call with confirm:true to apply.' };
|
|
359
|
+
}
|
|
328
360
|
const { status, data } = await api.post(
|
|
329
361
|
`/api/v1/projects/${args.projectId}/services/${args.service}/scale`,
|
|
330
362
|
{ replicas: args.replicas }
|
|
@@ -621,10 +653,16 @@ export const TOOLS = [
|
|
|
621
653
|
databaseId: str('Database id'),
|
|
622
654
|
volumeSize: str('New storage size, e.g. "20Gi" — must be >= current (grow-only)'),
|
|
623
655
|
replicas: int('Read/failover replicas (CNPG/postgres); total instances = 1 + replicas'),
|
|
656
|
+
confirm: { type: 'boolean', description: 'Approve the cost change: without confirm:true a storage/replica resize returns the new cost and applies nothing.' },
|
|
624
657
|
},
|
|
625
658
|
required: ['databaseId'],
|
|
626
659
|
},
|
|
627
660
|
async call(api, args) {
|
|
661
|
+
const costChanging = args.volumeSize || args.replicas !== undefined;
|
|
662
|
+
if (costChanging && args.confirm !== true) {
|
|
663
|
+
const gate = await confirmCost(api, args, { storageGB: gbFromSize(args.volumeSize), replicas: args.replicas });
|
|
664
|
+
if (gate) return { ...gate, change: 'resize database (storage/replicas)', message: 'Resizing changes cost — re-call with confirm:true to apply.' };
|
|
665
|
+
}
|
|
628
666
|
const body = {};
|
|
629
667
|
if (args.volumeSize) body.volumeSize = args.volumeSize;
|
|
630
668
|
if (args.replicas !== undefined) body.replicas = args.replicas;
|
|
@@ -787,11 +825,17 @@ export const TOOLS = [
|
|
|
787
825
|
dlqEnabled: { type: 'boolean', description: 'Enable/disable the dead-letter queue' },
|
|
788
826
|
volumeSize: str('New storage size, e.g. "20Gi" — must be >= current (grow-only)'),
|
|
789
827
|
retention: { type: 'object', description: 'Retention policy {maxAgeSeconds, maxBytes, maxMsgs}' },
|
|
828
|
+
confirm: { type: 'boolean', description: 'Approve the cost change: without confirm:true a storage/plan change returns the new cost and applies nothing. DLQ/retention-only edits apply directly.' },
|
|
790
829
|
},
|
|
791
830
|
required: ['queueId'],
|
|
792
831
|
},
|
|
793
832
|
async call(api, args) {
|
|
794
|
-
const
|
|
833
|
+
const costChanging = args.volumeSize || args.planClass;
|
|
834
|
+
if (costChanging && args.confirm !== true) {
|
|
835
|
+
const gate = await confirmCost(api, args, { storageGB: gbFromSize(args.volumeSize) });
|
|
836
|
+
if (gate) return { ...gate, change: 'update queue (storage/plan)', message: 'This changes cost — re-call with confirm:true to apply.' };
|
|
837
|
+
}
|
|
838
|
+
const { queueId, confirm, orgId, ...body } = args;
|
|
795
839
|
const { status, data } = await api.put(`/api/v1/queues/${queueId}`, body);
|
|
796
840
|
return redactSecrets(result(status, data));
|
|
797
841
|
},
|
|
@@ -820,8 +864,16 @@ export const TOOLS = [
|
|
|
820
864
|
],
|
|
821
865
|
branches: {
|
|
822
866
|
app_command: 'Provide serviceId (an existing service) OR image, plus command (argv array).',
|
|
823
|
-
http_endpoint: 'Provide url and method (GET/POST/…).',
|
|
867
|
+
http_endpoint: 'Provide url and method (GET/POST/…). Optional: headers (object), body + contentType, expectedStatus (exact code required for success), timeoutSeconds, retryCount, concurrencyPolicy (Allow|Forbid|Replace).',
|
|
824
868
|
},
|
|
869
|
+
httpOptions: [
|
|
870
|
+
{ field: 'headers', ask: 'Any request headers? (e.g. Authorization) — object', optional: true },
|
|
871
|
+
{ field: 'body', ask: 'A request body? (+ contentType, e.g. application/json)', optional: true },
|
|
872
|
+
{ field: 'expectedStatus', ask: 'A specific expected HTTP status for success? (default: any 2xx)', optional: true },
|
|
873
|
+
{ field: 'timeoutSeconds', ask: 'A per-run timeout (seconds)?', optional: true },
|
|
874
|
+
{ field: 'retryCount', ask: 'How many retries on failure?', optional: true },
|
|
875
|
+
{ field: 'concurrencyPolicy', ask: 'If a run overruns the next schedule: Allow | Forbid | Replace?', optional: true },
|
|
876
|
+
],
|
|
825
877
|
examples: [
|
|
826
878
|
{ intent: 'Run `node cleanup.js` in my web service every night at 2am Jakarta', args: { name: 'nightly-cleanup', mode: 'app_command', scheduleType: 'cron', schedule: '0 2 * * *', timezone: 'Asia/Jakarta', serviceId: '<web service id>', command: ['node', 'cleanup.js'], environment: 'staging' } },
|
|
827
879
|
{ intent: 'Ping a health URL every 15 minutes', args: { name: 'healthcheck', mode: 'http_endpoint', scheduleType: 'interval', intervalMinutes: 15, url: 'https://example.app/health', method: 'GET', environment: 'production' } },
|
|
@@ -849,6 +901,13 @@ export const TOOLS = [
|
|
|
849
901
|
command: { type: 'array', items: { type: 'string' }, description: 'Command argv (mode=app_command)' },
|
|
850
902
|
url: str('Target URL (mode=http_endpoint)'),
|
|
851
903
|
method: str('HTTP method (mode=http_endpoint), e.g. GET/POST'),
|
|
904
|
+
headers: { type: 'object', description: 'HTTP request headers (mode=http_endpoint), e.g. {"Authorization":"Bearer …"}. Secret header values are redacted when read back.' },
|
|
905
|
+
body: str('HTTP request body (mode=http_endpoint)'),
|
|
906
|
+
contentType: str('Content-Type for the body (mode=http_endpoint), e.g. "application/json" — merged into headers'),
|
|
907
|
+
expectedStatus: int('Exact HTTP status a run must receive to count as success (mode=http_endpoint; default: any 2xx)'),
|
|
908
|
+
timeoutSeconds: int('Per-run timeout in seconds (optional) → activeDeadlineSeconds'),
|
|
909
|
+
retryCount: int('Retries on failure (optional) → backoffLimit'),
|
|
910
|
+
concurrencyPolicy: { type: 'string', enum: ['Allow', 'Forbid', 'Replace'], description: 'Overlap policy if a run is still going when the next is due (optional, default Allow)' },
|
|
852
911
|
environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production (never defaulted, handoff §2)' },
|
|
853
912
|
confirm: { type: 'boolean', description: 'Approve & Create: without confirm:true the tool returns the cost (crons are billed per run — variable) and creates nothing. Re-call with confirm:true to create.' },
|
|
854
913
|
},
|
|
@@ -858,7 +917,11 @@ export const TOOLS = [
|
|
|
858
917
|
requireEnv(args.environment);
|
|
859
918
|
const gate = await confirmCost(api, args, {}); // cron cost is variable (per-run) — gate surfaces the rate
|
|
860
919
|
if (gate) return gate;
|
|
861
|
-
|
|
920
|
+
// Merge contentType into headers for convenience (unless already set).
|
|
921
|
+
if (args.contentType) {
|
|
922
|
+
args = { ...args, headers: { 'Content-Type': args.contentType, ...(args.headers || {}) } };
|
|
923
|
+
}
|
|
924
|
+
const { projectId, confirm, orgId, contentType, ...body } = args;
|
|
862
925
|
const { status, data } = await api.post(`/api/v1/projects/${projectId}/crons`, body);
|
|
863
926
|
return redactSecrets(result(status, data));
|
|
864
927
|
},
|
|
@@ -1228,8 +1291,7 @@ export const TOOLS = [
|
|
|
1228
1291
|
destructive: true,
|
|
1229
1292
|
schema: { type: 'object', properties: { appId: str('Service/app id (from neo_list_services)') }, required: ['appId'] },
|
|
1230
1293
|
async call(api, args) {
|
|
1231
|
-
|
|
1232
|
-
return result(status, data);
|
|
1294
|
+
return deleteAndConverge(api, `/api/v1/apps/${args.appId}`, args.appId, 'service');
|
|
1233
1295
|
},
|
|
1234
1296
|
},
|
|
1235
1297
|
{
|
|
@@ -1239,8 +1301,7 @@ export const TOOLS = [
|
|
|
1239
1301
|
destructive: true,
|
|
1240
1302
|
schema: { type: 'object', properties: { databaseId: str('Database id') }, required: ['databaseId'] },
|
|
1241
1303
|
async call(api, args) {
|
|
1242
|
-
|
|
1243
|
-
return result(status, data);
|
|
1304
|
+
return deleteAndConverge(api, `/api/v1/databases/${args.databaseId}`, args.databaseId, 'database');
|
|
1244
1305
|
},
|
|
1245
1306
|
},
|
|
1246
1307
|
{
|
|
@@ -1250,8 +1311,7 @@ export const TOOLS = [
|
|
|
1250
1311
|
destructive: true,
|
|
1251
1312
|
schema: { type: 'object', properties: { queueId: str('Queue id') }, required: ['queueId'] },
|
|
1252
1313
|
async call(api, args) {
|
|
1253
|
-
|
|
1254
|
-
return result(status, data);
|
|
1314
|
+
return deleteAndConverge(api, `/api/v1/queues/${args.queueId}`, args.queueId, 'queue');
|
|
1255
1315
|
},
|
|
1256
1316
|
},
|
|
1257
1317
|
{
|
|
@@ -1261,8 +1321,7 @@ export const TOOLS = [
|
|
|
1261
1321
|
destructive: true,
|
|
1262
1322
|
schema: { type: 'object', properties: { cronId: str('Cron/scheduled-job id') }, required: ['cronId'] },
|
|
1263
1323
|
async call(api, args) {
|
|
1264
|
-
|
|
1265
|
-
return result(status, data);
|
|
1324
|
+
return deleteAndConverge(api, `/api/v1/crons/${args.cronId}`, args.cronId, 'cron');
|
|
1266
1325
|
},
|
|
1267
1326
|
},
|
|
1268
1327
|
{
|
|
@@ -1272,8 +1331,7 @@ export const TOOLS = [
|
|
|
1272
1331
|
destructive: true,
|
|
1273
1332
|
schema: { type: 'object', properties: { bucketId: str('Bucket id') }, required: ['bucketId'] },
|
|
1274
1333
|
async call(api, args) {
|
|
1275
|
-
|
|
1276
|
-
return result(status, data);
|
|
1334
|
+
return deleteAndConverge(api, `/api/v1/buckets/${args.bucketId}`, args.bucketId, 'bucket');
|
|
1277
1335
|
},
|
|
1278
1336
|
},
|
|
1279
1337
|
{
|