@biznetgio/neo-app 0.4.27 → 0.4.29
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 +156 -8
package/package.json
CHANGED
package/src/mcp/tools.js
CHANGED
|
@@ -39,6 +39,32 @@ function requireEnv(env) {
|
|
|
39
39
|
return e;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// gbFromSize parses "10Gi"/"10G"/"10" → 10 (integer GB), else undefined.
|
|
43
|
+
function gbFromSize(v) {
|
|
44
|
+
if (v == null) return undefined;
|
|
45
|
+
const m = String(v).match(/(\d+)/);
|
|
46
|
+
return m ? parseInt(m[1], 10) : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// confirmCost is the Approve & Create gate for billable one-shot creates: unless
|
|
50
|
+
// args.confirm===true it returns the live-priced cost and creates NOTHING — the
|
|
51
|
+
// agent shows the user, then re-calls with confirm:true. plan/apply remains the
|
|
52
|
+
// server-immutable costed path; this is the lighter client-side gate (handoff 3.3).
|
|
53
|
+
async function confirmCost(api, args, spec) {
|
|
54
|
+
if (args.confirm === true) return null;
|
|
55
|
+
let orgId = null;
|
|
56
|
+
try { orgId = await api.resolveOrgId(args.orgId); } catch { orgId = null; }
|
|
57
|
+
let book = null;
|
|
58
|
+
try { book = (await api.get(orgId ? `/api/v1/orgs/${orgId}/pricing` : '/api/v1/pricing')).data; }
|
|
59
|
+
catch { try { book = (await api.get('/api/v1/pricing')).data; } catch { book = null; } }
|
|
60
|
+
return {
|
|
61
|
+
requiresConfirmation: true,
|
|
62
|
+
cost: book ? estimateCost(book, spec) : null,
|
|
63
|
+
priceBook: orgId ? `org:${orgId}` : 'global-defaults',
|
|
64
|
+
message: 'Billable resource — show the user this cost, then re-call with confirm:true to create. For an immutable, server-pinned quote use neo_plan_stack → neo_apply_stack.',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
42
68
|
const SECRET_KEY = /(password|secret|secretkey|accesskey|connectionstring|connstr|dsn|token|privatekey|apikey)/i;
|
|
43
69
|
function redactSecrets(value) {
|
|
44
70
|
if (Array.isArray(value)) return value.map(redactSecrets);
|
|
@@ -94,8 +120,8 @@ export const TOOLS = [
|
|
|
94
120
|
{ type: 'template', create: true, deploy: true, scale: false, logs: true, variables: true, delete: true },
|
|
95
121
|
{ type: 'static_site', create: 'via_folder', deploy: true, scale: false, logs: true, variables: true, delete: true, nativeStatic: true, note: 'neo_deploy_folder serviceType=static_site with buildCommand/publishDirectory → native nginx static hosting (0.4.27); auto-detected for static-only folders' },
|
|
96
122
|
{ type: 'worker', create: 'via_folder', deploy: true, scale: true, logs: true, variables: true, delete: true, note: 'neo_deploy_folder serviceType=worker — no public ingress; repo auto-detection still TODO (handoff 6)' },
|
|
97
|
-
{ type: 'database', create: true, get: true, list: true, update: true, backup: true, delete: true, note: 'neo_create/list/get/update(resize)/delete_database + backup (
|
|
98
|
-
{ type: 'queue', create: true, get: true, list: true, update: true, delete: true, note: 'neo_create/list/get/update/delete_queue
|
|
123
|
+
{ type: 'database', create: true, get: true, list: true, update: true, backup: true, verify: true, publicAccess: true, delete: true, note: 'neo_create/list/get/update(resize)/delete_database + backup + verify (SQL round-trip probe) + enable/disable_db_public_access; restore still TODO (handoff 4.4)' },
|
|
124
|
+
{ type: 'queue', create: true, get: true, list: true, update: true, publicAccess: true, delete: true, note: 'neo_create/list/get/update/delete_queue + enable/disable_queue_public_access (handoff 4.5)' },
|
|
99
125
|
{ type: 'cron', create: true, get: true, list: true, run: true, runs: true, delete: true, note: 'neo_create/list/get/run/list_runs/delete_cron (0.4.15-0.4.21) (handoff 4.6)' },
|
|
100
126
|
{ type: 'object_storage', create: true, get: true, list: true, objects: true, upload: true, delete: true, note: 'neo_create/list/get/delete_bucket + list/upload/presign(get)/delete objects (0.4.15-0.4.25) (handoff 4.7)' },
|
|
101
127
|
];
|
|
@@ -105,7 +131,7 @@ export const TOOLS = [
|
|
|
105
131
|
controlPlane,
|
|
106
132
|
resourceTypes,
|
|
107
133
|
// Verbs this MCP build exposes today (no create-DB/queue/cron/bucket, no costed plan/apply yet).
|
|
108
|
-
lifecycleVerbs: ['plan', 'apply', 'get_operation', 'wait_operation', 'delete_stack', 'reconcile_usage', 'create', 'list', 'get', 'update', 'deploy', 'scale', 'logs', 'get_variables', 'set_variable', 'estimate', 'scan_repo', 'bind', 'unbind', 'run_cron', 'presign_object', 'backup', 'delete', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
|
|
134
|
+
lifecycleVerbs: ['plan', 'apply', 'get_operation', 'wait_operation', 'delete_stack', 'reconcile_usage', 'service_usage', 'create', 'list', 'get', 'update', 'verify', 'enable_public_access', 'disable_public_access', 'deploy', 'scale', 'logs', 'get_variables', 'set_variable', 'estimate', 'scan_repo', 'bind', 'unbind', 'run_cron', 'presign_object', 'backup', 'delete', 'delete_project', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
|
|
109
135
|
bindings: { supported: true, targets: ['neo_db', 'neo_queue'], writeOnly: true, note: 'neo_bind_resource injects DATABASE_URL/NATS_URL server-side at deploy; the secret is never returned (handoff 5)' },
|
|
110
136
|
sourceTypes: ['git', 'image', 'dockerfile', 'local', 'template'],
|
|
111
137
|
environments: ['staging', 'production'],
|
|
@@ -293,6 +319,25 @@ export const TOOLS = [
|
|
|
293
319
|
return (await api.get(`/api/v1/orgs/${orgId}/credit`)).data;
|
|
294
320
|
},
|
|
295
321
|
},
|
|
322
|
+
{
|
|
323
|
+
name: 'neo_service_usage',
|
|
324
|
+
description: 'Get the metered usage + cost attributed to ONE service/resource in a project — per-service usage attribution (handoff §4). Filters the project usage breakdown to this resourceId.',
|
|
325
|
+
mutating: false,
|
|
326
|
+
schema: { type: 'object', properties: { projectId: str('Project id'), resourceId: str('Service/database/queue/bucket id (from neo_list_services/databases/…)') }, required: ['projectId', 'resourceId'] },
|
|
327
|
+
async call(api, args) {
|
|
328
|
+
const u = (await api.get(`/api/v1/projects/${args.projectId}/usage`)).data || {};
|
|
329
|
+
const lines = (u.lines || []).filter((l) => l.resourceId === args.resourceId);
|
|
330
|
+
const totalCost = Math.round(lines.reduce((sum, l) => sum + (l.cost || 0), 0));
|
|
331
|
+
return {
|
|
332
|
+
resourceId: args.resourceId,
|
|
333
|
+
period: u.period,
|
|
334
|
+
currency: u.currency,
|
|
335
|
+
lines,
|
|
336
|
+
totalCost,
|
|
337
|
+
note: lines.length ? undefined : 'no metered usage attributed to this resource yet (usage accrues hourly)',
|
|
338
|
+
};
|
|
339
|
+
},
|
|
340
|
+
},
|
|
296
341
|
{
|
|
297
342
|
name: 'neo_estimate',
|
|
298
343
|
description:
|
|
@@ -477,12 +522,15 @@ export const TOOLS = [
|
|
|
477
522
|
volumeSize: str('Storage size, e.g. "10Gi" (optional)'),
|
|
478
523
|
replicas: int('Read/failover replicas for HA (optional; postgres/CNPG only)'),
|
|
479
524
|
deploymentMode: { type: 'string', description: 'single | ha (optional)' },
|
|
525
|
+
confirm: { type: 'boolean', description: 'Approve & Create: without confirm:true the tool returns the cost and creates nothing. Re-call with confirm:true to actually create.' },
|
|
480
526
|
},
|
|
481
527
|
required: ['projectId', 'name', 'engine', 'environment'],
|
|
482
528
|
},
|
|
483
529
|
async call(api, args) {
|
|
484
530
|
requireEnv(args.environment);
|
|
485
|
-
const
|
|
531
|
+
const gate = await confirmCost(api, args, { storageGB: gbFromSize(args.volumeSize), replicas: args.replicas });
|
|
532
|
+
if (gate) return gate;
|
|
533
|
+
const { projectId, confirm, orgId, ...body } = args;
|
|
486
534
|
const { status, data } = await api.post(`/api/v1/projects/${projectId}/databases`, body);
|
|
487
535
|
return redactSecrets(result(status, data));
|
|
488
536
|
},
|
|
@@ -526,6 +574,16 @@ export const TOOLS = [
|
|
|
526
574
|
return redactSecrets(result(status, data));
|
|
527
575
|
},
|
|
528
576
|
},
|
|
577
|
+
{
|
|
578
|
+
name: 'neo_verify_database',
|
|
579
|
+
description: 'Functionally verify a managed database (handoff §8.1): runs an in-cluster probe that opens the DB and does a SQL round-trip (SELECT 1), returning {verified, log} — WITHOUT handing you any credentials. Postgres only for now. Synchronous (waits up to ~60s for the probe).',
|
|
580
|
+
mutating: true,
|
|
581
|
+
schema: { type: 'object', properties: { databaseId: str('Database id (postgres)') }, required: ['databaseId'] },
|
|
582
|
+
async call(api, args) {
|
|
583
|
+
const { status, data } = await api.post(`/api/v1/databases/${args.databaseId}/verify`);
|
|
584
|
+
return result(status, data);
|
|
585
|
+
},
|
|
586
|
+
},
|
|
529
587
|
{
|
|
530
588
|
name: 'neo_list_backups',
|
|
531
589
|
description: 'List configured database backups in a project (handoff 4.4): schedule, destination, last run status.',
|
|
@@ -577,13 +635,16 @@ export const TOOLS = [
|
|
|
577
635
|
planClass: str('Plan/engine class (optional)'),
|
|
578
636
|
environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production (never defaulted, handoff §2)' },
|
|
579
637
|
volumeSize: str('Storage size, e.g. "10Gi" (optional)'),
|
|
580
|
-
dlqEnabled: { type: 'boolean', description: 'Enable a dead-letter queue (optional)' },
|
|
638
|
+
dlqEnabled: { type: 'boolean', description: 'Enable a dead-letter queue (optional, default true). Pass false to disable — this is now honored.' },
|
|
639
|
+
confirm: { type: 'boolean', description: 'Approve & Create: without confirm:true the tool returns the cost and creates nothing. Re-call with confirm:true to actually create.' },
|
|
581
640
|
},
|
|
582
641
|
required: ['projectId', 'name', 'environment'],
|
|
583
642
|
},
|
|
584
643
|
async call(api, args) {
|
|
585
644
|
requireEnv(args.environment);
|
|
586
|
-
const
|
|
645
|
+
const gate = await confirmCost(api, args, { storageGB: gbFromSize(args.volumeSize) });
|
|
646
|
+
if (gate) return gate;
|
|
647
|
+
const { projectId, confirm, orgId, ...body } = args;
|
|
587
648
|
const { status, data } = await api.post(`/api/v1/projects/${projectId}/queues`, body);
|
|
588
649
|
return redactSecrets(result(status, data));
|
|
589
650
|
},
|
|
@@ -974,6 +1035,16 @@ export const TOOLS = [
|
|
|
974
1035
|
const plan = (await api.get(`/api/v1/plans/${args.planId}`)).data;
|
|
975
1036
|
let usage = null;
|
|
976
1037
|
try { usage = (await api.get(`/api/v1/projects/${plan.projectId}/usage`)).data; } catch { /* usage may be unavailable */ }
|
|
1038
|
+
// Per-resource actuals: attribute the project usage lines to this plan's resources.
|
|
1039
|
+
const planIds = new Set(Object.values(plan.result || {}).map((v) => v && v.id).filter(Boolean));
|
|
1040
|
+
const perResource = {};
|
|
1041
|
+
for (const l of (usage?.lines || [])) {
|
|
1042
|
+
if (!planIds.has(l.resourceId)) continue;
|
|
1043
|
+
const r = perResource[l.resourceId] || { resourceName: l.resourceName, resourceType: l.resourceType, cost: 0 };
|
|
1044
|
+
r.cost += l.cost || 0;
|
|
1045
|
+
perResource[l.resourceId] = r;
|
|
1046
|
+
}
|
|
1047
|
+
const actualForPlan = Math.round(Object.values(perResource).reduce((s, r) => s + r.cost, 0));
|
|
977
1048
|
return {
|
|
978
1049
|
planId: args.planId,
|
|
979
1050
|
projectId: plan.projectId,
|
|
@@ -981,8 +1052,10 @@ export const TOOLS = [
|
|
|
981
1052
|
currency: plan.cost?.currency,
|
|
982
1053
|
priceBook: plan.cost?.priceBook,
|
|
983
1054
|
variableSkus: plan.cost?.variable || [],
|
|
984
|
-
|
|
985
|
-
|
|
1055
|
+
actualForPlanResources: actualForPlan,
|
|
1056
|
+
perResource,
|
|
1057
|
+
projectUsageTotal: usage?.totalCost,
|
|
1058
|
+
note: 'estimatedMonthly is the FIXED monthly compute/storage estimate; actualForPlanResources sums the metered cost of THIS plan\'s resources so far (accrues hourly — ~0 just after apply). perResource breaks it down.',
|
|
986
1059
|
};
|
|
987
1060
|
},
|
|
988
1061
|
},
|
|
@@ -1044,4 +1117,79 @@ export const TOOLS = [
|
|
|
1044
1117
|
return result(status, data);
|
|
1045
1118
|
},
|
|
1046
1119
|
},
|
|
1120
|
+
{
|
|
1121
|
+
name: 'neo_enable_db_public_access',
|
|
1122
|
+
description: 'Enable internet access to a managed postgres database through the TLS gateway (phase 3). Provisions a least-privilege external role, assigns a public host, and flips publicEnabled. The DB must be DEPLOYED first. Optionally restrict to allowlistCidrs. Credentials are write-only — retrieve the full external connection string from the dashboard; this returns the public host/port, external username, and CA URL.',
|
|
1123
|
+
mutating: true,
|
|
1124
|
+
schema: {
|
|
1125
|
+
type: 'object',
|
|
1126
|
+
properties: {
|
|
1127
|
+
databaseId: str('Database id (postgres, already deployed)'),
|
|
1128
|
+
allowlistCidrs: { type: 'array', items: { type: 'string' }, description: 'CIDR allowlist, e.g. ["203.0.113.0/24"] (optional; empty = allow any source IP)' },
|
|
1129
|
+
},
|
|
1130
|
+
required: ['databaseId'],
|
|
1131
|
+
},
|
|
1132
|
+
async call(api, args) {
|
|
1133
|
+
const { status, data } = await api.post(`/api/v1/databases/${args.databaseId}/public-access`, { allowlistCidrs: args.allowlistCidrs || [] });
|
|
1134
|
+
return redactSecrets(result(status, data));
|
|
1135
|
+
},
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
name: 'neo_disable_db_public_access',
|
|
1139
|
+
description: 'Disable internet access to a managed database. Non-destructive: the external role is set NOLOGIN and its objects are preserved; routing stops.',
|
|
1140
|
+
mutating: true,
|
|
1141
|
+
schema: { type: 'object', properties: { databaseId: str('Database id') }, required: ['databaseId'] },
|
|
1142
|
+
async call(api, args) {
|
|
1143
|
+
const { status, data } = await api.del(`/api/v1/databases/${args.databaseId}/public-access`);
|
|
1144
|
+
return result(status, data);
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
name: 'neo_enable_queue_public_access',
|
|
1149
|
+
description: 'Enable internet access to a managed queue through the TLS gateway (phase 3). Assigns a public host and flips publicEnabled. The queue must be DEPLOYED first. Optionally restrict to allowlistCidrs. Credentials are write-only — retrieve the full connection string from the dashboard.',
|
|
1150
|
+
mutating: true,
|
|
1151
|
+
schema: {
|
|
1152
|
+
type: 'object',
|
|
1153
|
+
properties: {
|
|
1154
|
+
queueId: str('Queue id (already deployed)'),
|
|
1155
|
+
allowlistCidrs: { type: 'array', items: { type: 'string' }, description: 'CIDR allowlist (optional; empty = allow any source IP)' },
|
|
1156
|
+
},
|
|
1157
|
+
required: ['queueId'],
|
|
1158
|
+
},
|
|
1159
|
+
async call(api, args) {
|
|
1160
|
+
const { status, data } = await api.post(`/api/v1/queues/${args.queueId}/public-access`, { allowlistCidrs: args.allowlistCidrs || [] });
|
|
1161
|
+
return redactSecrets(result(status, data));
|
|
1162
|
+
},
|
|
1163
|
+
},
|
|
1164
|
+
{
|
|
1165
|
+
name: 'neo_disable_queue_public_access',
|
|
1166
|
+
description: 'Disable internet access to a managed queue. Non-destructive: routing stops; the queue keeps running for in-cluster consumers.',
|
|
1167
|
+
mutating: true,
|
|
1168
|
+
schema: { type: 'object', properties: { queueId: str('Queue id') }, required: ['queueId'] },
|
|
1169
|
+
async call(api, args) {
|
|
1170
|
+
const { status, data } = await api.del(`/api/v1/queues/${args.queueId}/public-access`);
|
|
1171
|
+
return result(status, data);
|
|
1172
|
+
},
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
name: 'neo_delete_project',
|
|
1176
|
+
description: 'Permanently delete a project and ALL its resources (apps, databases, queues, crons, buckets, and the project namespace with its PVCs/CNPG clusters). Destructive and irreversible — nothing is recoverable. Requires confirm:true. Only the project creator or an org owner/admin may delete it.',
|
|
1177
|
+
mutating: true,
|
|
1178
|
+
destructive: true,
|
|
1179
|
+
schema: {
|
|
1180
|
+
type: 'object',
|
|
1181
|
+
properties: {
|
|
1182
|
+
projectId: str('Project id'),
|
|
1183
|
+
confirm: { type: 'boolean', description: 'Must be true to proceed — guards against accidental deletion of an entire project.' },
|
|
1184
|
+
},
|
|
1185
|
+
required: ['projectId', 'confirm'],
|
|
1186
|
+
},
|
|
1187
|
+
async call(api, args) {
|
|
1188
|
+
if (args.confirm !== true) {
|
|
1189
|
+
return { error: 'refused', message: 'Deleting a project removes ALL its resources and is irreversible. Re-call with confirm:true to proceed.' };
|
|
1190
|
+
}
|
|
1191
|
+
const { status, data } = await api.del(`/api/v1/projects/${args.projectId}`);
|
|
1192
|
+
return result(status, data);
|
|
1193
|
+
},
|
|
1194
|
+
},
|
|
1047
1195
|
];
|