@biznetgio/neo-app 0.4.24 → 0.4.26
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/api.js +12 -0
- package/src/mcp/tools.js +140 -6
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -58,6 +58,18 @@ export class NeoApi {
|
|
|
58
58
|
return { status: res.status, data: payload };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// postForm sends a multipart/form-data body (FormData); fetch sets the boundary.
|
|
62
|
+
async postForm(path, form) {
|
|
63
|
+
const headers = {};
|
|
64
|
+
if (this.token) headers.Authorization = `Bearer ${this.token}`;
|
|
65
|
+
const res = await fetch(this.baseUrl + path, { method: 'POST', headers, body: form });
|
|
66
|
+
const payload = await res.json().catch(() => null);
|
|
67
|
+
if (res.status >= 400) {
|
|
68
|
+
throw new ApiError(res.status, (payload && payload.error) || res.statusText, payload);
|
|
69
|
+
}
|
|
70
|
+
return { status: res.status, data: payload };
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
get(path, opts) {
|
|
62
74
|
return this.request('GET', path, opts);
|
|
63
75
|
}
|
package/src/mcp/tools.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// map). Read-only vs mutating is annotated; mutating tools surface the server's
|
|
3
3
|
// 202 pending_approval result verbatim instead of treating it as success/failure.
|
|
4
4
|
|
|
5
|
-
import { resolve } from 'node:path';
|
|
6
|
-
import { statSync } from 'node:fs';
|
|
5
|
+
import { resolve, basename } from 'node:path';
|
|
6
|
+
import { statSync, readFileSync } from 'node:fs';
|
|
7
7
|
import { deploySource } from '../deploySource.js';
|
|
8
8
|
import { pkgVersion } from '../util.js';
|
|
9
9
|
import { estimateCost } from '../pricing.js';
|
|
@@ -94,10 +94,10 @@ export const TOOLS = [
|
|
|
94
94
|
{ type: 'template', create: true, deploy: true, scale: false, logs: true, variables: true, delete: true },
|
|
95
95
|
{ type: 'static_site', create: 'via_folder', deploy: true, scale: false, logs: true, variables: true, delete: true, note: 'neo_deploy_folder serviceType=static_site (auto-detected for static-only folders); auto-detection from a repo/monorepo is still TODO (handoff 6)' },
|
|
96
96
|
{ 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, delete: true, note: 'neo_create/list/get/update(resize)/delete_database (
|
|
98
|
-
{ type: 'queue', create: true, get: true, list: true, delete: true, note: 'neo_create/list/get/delete_queue (0.4.15)
|
|
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 (list/create/run); restore still TODO (handoff 4.4)' },
|
|
98
|
+
{ type: 'queue', create: true, get: true, list: true, update: true, delete: true, note: 'neo_create/list/get/update/delete_queue (0.4.15-0.4.25) (handoff 4.5)' },
|
|
99
99
|
{ 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
|
-
{ type: 'object_storage', create: true, get: true, list: true, objects: true, delete: true, note: 'neo_create/list/get/delete_bucket + list/delete objects (0.4.15-0.4.
|
|
100
|
+
{ 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
101
|
];
|
|
102
102
|
|
|
103
103
|
return {
|
|
@@ -105,7 +105,7 @@ export const TOOLS = [
|
|
|
105
105
|
controlPlane,
|
|
106
106
|
resourceTypes,
|
|
107
107
|
// 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', 'create', 'list', 'get', 'update', 'deploy', 'scale', 'logs', 'get_variables', 'set_variable', 'estimate', 'scan_repo', 'bind', 'unbind', 'run_cron', 'presign_object', 'delete', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
|
|
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'],
|
|
109
109
|
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
110
|
sourceTypes: ['git', 'image', 'dockerfile', 'local', 'template'],
|
|
111
111
|
environments: ['staging', 'production'],
|
|
@@ -349,6 +349,11 @@ export const TOOLS = [
|
|
|
349
349
|
suggestedBuildType: s.type === 'docker' ? 'dockerfile' : 'railpack',
|
|
350
350
|
contextDir: s.path || undefined,
|
|
351
351
|
detectedDependencies: s.deps || [], // database | queue | object_storage, inferred from the manifest
|
|
352
|
+
// Confidence + evidence (handoff 6): an explicit Dockerfile/index.html is high
|
|
353
|
+
// confidence; a language marker is inferred (medium). The stack marker + any
|
|
354
|
+
// detectedDependencies ARE the evidence.
|
|
355
|
+
typeConfidence: (s.type === 'docker' || s.type === 'static') ? 'high' : 'medium',
|
|
356
|
+
evidence: { marker: s.type, manifestDeps: s.deps || [] },
|
|
352
357
|
}));
|
|
353
358
|
const anyDeps = components.some((c) => c.detectedDependencies.length);
|
|
354
359
|
return {
|
|
@@ -517,6 +522,45 @@ export const TOOLS = [
|
|
|
517
522
|
return redactSecrets(result(status, data));
|
|
518
523
|
},
|
|
519
524
|
},
|
|
525
|
+
{
|
|
526
|
+
name: 'neo_list_backups',
|
|
527
|
+
description: 'List configured database backups in a project (handoff 4.4): schedule, destination, last run status.',
|
|
528
|
+
mutating: false,
|
|
529
|
+
schema: { type: 'object', properties: { projectId: str('Project id') }, required: ['projectId'] },
|
|
530
|
+
async call(api, args) {
|
|
531
|
+
return redactSecrets((await api.get(`/api/v1/projects/${args.projectId}/backups`)).data);
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
name: 'neo_create_backup',
|
|
536
|
+
description: 'Configure a backup for a managed database (handoff 4.4). `cron` sets a schedule (empty = manual only); `destination` is local (default) or s3. Trigger it with neo_run_backup.',
|
|
537
|
+
mutating: true,
|
|
538
|
+
schema: {
|
|
539
|
+
type: 'object',
|
|
540
|
+
properties: {
|
|
541
|
+
projectId: str('Project id'),
|
|
542
|
+
databaseId: str('Database id to back up'),
|
|
543
|
+
cron: str('Cron schedule (optional; empty = manual)'),
|
|
544
|
+
destination: { type: 'string', enum: ['local', 's3'], description: 'local (default) | s3' },
|
|
545
|
+
},
|
|
546
|
+
required: ['projectId', 'databaseId'],
|
|
547
|
+
},
|
|
548
|
+
async call(api, args) {
|
|
549
|
+
const { projectId, ...body } = args;
|
|
550
|
+
const { status, data } = await api.post(`/api/v1/projects/${projectId}/backups`, body);
|
|
551
|
+
return redactSecrets(result(status, data));
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
name: 'neo_run_backup',
|
|
556
|
+
description: 'Trigger a configured database backup to run now (handoff 4.4).',
|
|
557
|
+
mutating: true,
|
|
558
|
+
schema: { type: 'object', properties: { backupId: str('Backup id (from neo_list_backups)') }, required: ['backupId'] },
|
|
559
|
+
async call(api, args) {
|
|
560
|
+
const { status, data } = await api.post(`/api/v1/backups/${args.backupId}/run`);
|
|
561
|
+
return redactSecrets(result(status, data));
|
|
562
|
+
},
|
|
563
|
+
},
|
|
520
564
|
{
|
|
521
565
|
name: 'neo_create_queue',
|
|
522
566
|
description: 'Create a managed queue in a project (handoff 4.5). Credentials redacted. May return pending_approval.',
|
|
@@ -558,6 +602,27 @@ export const TOOLS = [
|
|
|
558
602
|
return redactSecrets((await api.get(`/api/v1/queues/${args.queueId}`)).data);
|
|
559
603
|
},
|
|
560
604
|
},
|
|
605
|
+
{
|
|
606
|
+
name: 'neo_update_queue',
|
|
607
|
+
description: 'Update a managed queue (handoff 4.5): plan class, DLQ, retention, and storage (grow-only — a smaller volumeSize is rejected). Redeploy the queue for it to take effect.',
|
|
608
|
+
mutating: true,
|
|
609
|
+
schema: {
|
|
610
|
+
type: 'object',
|
|
611
|
+
properties: {
|
|
612
|
+
queueId: str('Queue id'),
|
|
613
|
+
planClass: str('dev | standard'),
|
|
614
|
+
dlqEnabled: { type: 'boolean', description: 'Enable/disable the dead-letter queue' },
|
|
615
|
+
volumeSize: str('New storage size, e.g. "20Gi" — must be >= current (grow-only)'),
|
|
616
|
+
retention: { type: 'object', description: 'Retention policy {maxAgeSeconds, maxBytes, maxMsgs}' },
|
|
617
|
+
},
|
|
618
|
+
required: ['queueId'],
|
|
619
|
+
},
|
|
620
|
+
async call(api, args) {
|
|
621
|
+
const { queueId, ...body } = args;
|
|
622
|
+
const { status, data } = await api.put(`/api/v1/queues/${queueId}`, body);
|
|
623
|
+
return redactSecrets(result(status, data));
|
|
624
|
+
},
|
|
625
|
+
},
|
|
561
626
|
{
|
|
562
627
|
name: 'neo_create_cron',
|
|
563
628
|
description: 'Create a scheduled job (cron) in a project (handoff 4.6). Target is either an existing service command (mode=app_command, serviceId+command) or an HTTP endpoint (mode=http_endpoint, url+method). Schedule via scheduleType (cron|interval|once|seconds) + schedule/intervalMinutes. May return pending_approval.',
|
|
@@ -693,6 +758,31 @@ export const TOOLS = [
|
|
|
693
758
|
return (await api.get(`/api/v1/buckets/${args.bucketId}/objects/presign`, { query: { object: args.object } })).data;
|
|
694
759
|
},
|
|
695
760
|
},
|
|
761
|
+
{
|
|
762
|
+
name: 'neo_upload_bucket_object',
|
|
763
|
+
description: 'Upload a LOCAL file to an object-storage bucket (handoff 4.7 — object put). The path is on the machine running this MCP server. objectName defaults to the file\'s basename.',
|
|
764
|
+
mutating: true,
|
|
765
|
+
schema: {
|
|
766
|
+
type: 'object',
|
|
767
|
+
properties: {
|
|
768
|
+
bucketId: str('Bucket id'),
|
|
769
|
+
filePath: str('Local file path to upload'),
|
|
770
|
+
objectName: str('Object key/name in the bucket (optional; default the file basename)'),
|
|
771
|
+
},
|
|
772
|
+
required: ['bucketId', 'filePath'],
|
|
773
|
+
},
|
|
774
|
+
async call(api, args) {
|
|
775
|
+
const p = resolve(args.filePath);
|
|
776
|
+
const st = statSync(p);
|
|
777
|
+
if (!st.isFile()) throw new Error(`not a file: ${args.filePath}`);
|
|
778
|
+
const name = args.objectName || basename(p);
|
|
779
|
+
const form = new FormData();
|
|
780
|
+
form.append('object_name', name);
|
|
781
|
+
form.append('file', new Blob([readFileSync(p)]), name);
|
|
782
|
+
const { status, data } = await api.postForm(`/api/v1/buckets/${args.bucketId}/objects`, form);
|
|
783
|
+
return result(status, data);
|
|
784
|
+
},
|
|
785
|
+
},
|
|
696
786
|
// --- Typed bindings (handoff 5): attach a DB/queue to a consumer service. The
|
|
697
787
|
// connection string is injected as an env var SERVER-SIDE at the consumer's next
|
|
698
788
|
// deploy (marked a secret) — it is never returned to the agent (write-only). ---
|
|
@@ -846,6 +936,50 @@ export const TOOLS = [
|
|
|
846
936
|
return op;
|
|
847
937
|
},
|
|
848
938
|
},
|
|
939
|
+
{
|
|
940
|
+
name: 'neo_delete_stack',
|
|
941
|
+
description:
|
|
942
|
+
'Tear down a whole stack created by neo_apply_stack — deletes its resources in REVERSE dependency order (services first, then databases/queues/crons/buckets, handoff 3.6/7). Destructive and irreversible. Pass the operationId/planId from the apply.',
|
|
943
|
+
mutating: true,
|
|
944
|
+
destructive: true,
|
|
945
|
+
schema: { type: 'object', properties: { operationId: str('Operation/plan id from neo_apply_stack') }, required: ['operationId'] },
|
|
946
|
+
async call(api, args) {
|
|
947
|
+
const op = (await api.get(`/api/v1/plans/${args.operationId}`)).data;
|
|
948
|
+
const entries = Object.entries(op.result || {}).map(([name, v]) => ({ name, kind: v.kind, id: v.id })).filter((e) => e.id);
|
|
949
|
+
const rank = { app: 0, cron: 1, database: 2, queue: 2, bucket: 2 }; // services before foundations
|
|
950
|
+
entries.sort((a, b) => (rank[a.kind] ?? 3) - (rank[b.kind] ?? 3));
|
|
951
|
+
const path = { app: 'apps', database: 'databases', queue: 'queues', cron: 'crons', bucket: 'buckets' };
|
|
952
|
+
const deleted = [];
|
|
953
|
+
for (const e of entries) {
|
|
954
|
+
if (!path[e.kind]) continue;
|
|
955
|
+
try { await api.del(`/api/v1/${path[e.kind]}/${e.id}`); deleted.push({ name: e.name, kind: e.kind, status: 'deleted' }); }
|
|
956
|
+
catch (err) { deleted.push({ name: e.name, kind: e.kind, status: 'failed', error: err.message }); }
|
|
957
|
+
}
|
|
958
|
+
return { operationId: args.operationId, order: 'reverse-dependency (services → foundations)', deleted };
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
name: 'neo_reconcile_usage',
|
|
963
|
+
description:
|
|
964
|
+
'Reconcile an approved plan\'s estimated cost against actual metered usage (handoff A2). Returns the plan\'s fixed monthly estimate + price-book snapshot + the variable SKUs, and the project\'s actual usage, so billing can be checked against what was approved. Usage accrues hourly — a freshly-applied stack shows ~0 until it runs.',
|
|
965
|
+
mutating: false,
|
|
966
|
+
schema: { type: 'object', properties: { planId: str('Plan id from neo_plan_stack/apply') }, required: ['planId'] },
|
|
967
|
+
async call(api, args) {
|
|
968
|
+
const plan = (await api.get(`/api/v1/plans/${args.planId}`)).data;
|
|
969
|
+
let usage = null;
|
|
970
|
+
try { usage = (await api.get(`/api/v1/projects/${plan.projectId}/usage`)).data; } catch { /* usage may be unavailable */ }
|
|
971
|
+
return {
|
|
972
|
+
planId: args.planId,
|
|
973
|
+
projectId: plan.projectId,
|
|
974
|
+
estimatedMonthly: plan.cost?.monthly ?? null,
|
|
975
|
+
currency: plan.cost?.currency,
|
|
976
|
+
priceBook: plan.cost?.priceBook,
|
|
977
|
+
variableSkus: plan.cost?.variable || [],
|
|
978
|
+
actualUsage: redactSecrets(usage),
|
|
979
|
+
note: 'estimatedMonthly is the FIXED compute/storage estimate; actualUsage is metered (incl. variable usage). Compare over a full billing period — a just-applied stack has not accrued usage yet.',
|
|
980
|
+
};
|
|
981
|
+
},
|
|
982
|
+
},
|
|
849
983
|
// --- Cleanup (handoff 3.6): MCP can delete every resource type it can reach, so
|
|
850
984
|
// an agent never leaks a resource it created. These are DESTRUCTIVE and permanent;
|
|
851
985
|
// the server hard-deletes the record and tears down the running workload. ---
|