@biznetgio/neo-app 0.4.16 → 0.4.18
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 +81 -11
package/package.json
CHANGED
package/src/mcp/tools.js
CHANGED
|
@@ -28,6 +28,17 @@ function result(status, data) {
|
|
|
28
28
|
// (password, connection string, access/secret key, token) back to the agent.
|
|
29
29
|
// Recursively replace any secret-looking value with a reference marker so the
|
|
30
30
|
// agent still sees the resource's shape and non-secret identifiers.
|
|
31
|
+
// §2 safety: a create/deploy mutation must name its environment explicitly —
|
|
32
|
+
// NEVER default to production. A silent prod mutation is the exact risk the
|
|
33
|
+
// handoff calls out ("Never default a missing environment to production").
|
|
34
|
+
function requireEnv(env) {
|
|
35
|
+
const e = String(env || '').toLowerCase();
|
|
36
|
+
if (e !== 'staging' && e !== 'production') {
|
|
37
|
+
throw new Error('environment is required and must be "staging" or "production" — it is never defaulted (handoff §2: never silently mutate production). Test on staging first.');
|
|
38
|
+
}
|
|
39
|
+
return e;
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
const SECRET_KEY = /(password|secret|secretkey|accesskey|connectionstring|connstr|dsn|token|privatekey|apikey)/i;
|
|
32
43
|
function redactSecrets(value) {
|
|
33
44
|
if (Array.isArray(value)) return value.map(redactSecrets);
|
|
@@ -85,7 +96,7 @@ export const TOOLS = [
|
|
|
85
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)' },
|
|
86
97
|
{ type: 'database', create: true, get: true, list: true, delete: true, note: 'neo_create/list/get/delete_database (0.4.15); update/plan still TODO (handoff 4.4)' },
|
|
87
98
|
{ type: 'queue', create: true, get: true, list: true, delete: true, note: 'neo_create/list/get/delete_queue (0.4.15); update/plan still TODO (handoff 4.5)' },
|
|
88
|
-
{ type: 'cron', create: true, get: true, list: true, delete: true, note: 'neo_create/list/get/delete_cron (0.4.15);
|
|
99
|
+
{ type: 'cron', create: true, get: true, list: true, run: true, delete: true, note: 'neo_create/list/get/run/delete_cron (0.4.15-0.4.17); run-history still TODO (handoff 4.6)' },
|
|
89
100
|
{ type: 'object_storage', create: true, get: true, list: true, delete: true, note: 'neo_create/list/get/delete_bucket (0.4.15); object put/get still TODO (handoff 4.7)' },
|
|
90
101
|
];
|
|
91
102
|
|
|
@@ -94,7 +105,8 @@ export const TOOLS = [
|
|
|
94
105
|
controlPlane,
|
|
95
106
|
resourceTypes,
|
|
96
107
|
// Verbs this MCP build exposes today (no create-DB/queue/cron/bucket, no costed plan/apply yet).
|
|
97
|
-
lifecycleVerbs: ['create', 'list', 'get', 'deploy', 'scale', 'logs', 'get_variables', 'set_variable', 'delete', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
|
|
108
|
+
lifecycleVerbs: ['create', 'list', 'get', 'deploy', 'scale', 'logs', 'get_variables', 'set_variable', 'estimate', 'bind', 'unbind', 'run_cron', 'delete', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
|
|
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)' },
|
|
98
110
|
sourceTypes: ['git', 'image', 'dockerfile', 'local', 'template'],
|
|
99
111
|
environments: ['staging', 'production'],
|
|
100
112
|
pricing,
|
|
@@ -329,12 +341,13 @@ export const TOOLS = [
|
|
|
329
341
|
name: str('Service name (optional)'),
|
|
330
342
|
serviceType: { type: 'string', enum: ['web', 'worker', 'static_site'], description: 'web (default) | worker (no public URL) | static_site. Auto-detects static_site for a static-only folder; else web.' },
|
|
331
343
|
port: int('Container port (optional; ignored for worker)'),
|
|
332
|
-
environment:
|
|
344
|
+
environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production. Never defaulted (handoff §2). Deploy to staging first.' },
|
|
333
345
|
dryRun: { type: 'boolean', description: 'Preview the plan without creating/uploading/deploying anything (JNG #93)' },
|
|
334
346
|
},
|
|
335
|
-
required: ['projectId', 'path'],
|
|
347
|
+
required: ['projectId', 'path', 'environment'],
|
|
336
348
|
},
|
|
337
349
|
async call(api, args) {
|
|
350
|
+
const environment = requireEnv(args.environment);
|
|
338
351
|
const p = resolve(args.path);
|
|
339
352
|
let src;
|
|
340
353
|
try {
|
|
@@ -351,7 +364,7 @@ export const TOOLS = [
|
|
|
351
364
|
name: args.name,
|
|
352
365
|
serviceType: args.serviceType,
|
|
353
366
|
port: args.port,
|
|
354
|
-
environment
|
|
367
|
+
environment,
|
|
355
368
|
dryRun: !!args.dryRun,
|
|
356
369
|
});
|
|
357
370
|
if (r.dryRun) return { status: 'dry_run', plan: r.plan, service: r.service, environment: r.environment, estimatedCost: r.estimatedCost };
|
|
@@ -390,14 +403,15 @@ export const TOOLS = [
|
|
|
390
403
|
name: str('Database name'),
|
|
391
404
|
engine: { type: 'string', description: 'postgres | mysql (engine)' },
|
|
392
405
|
version: str('Engine version, e.g. "16" (optional)'),
|
|
393
|
-
environment:
|
|
406
|
+
environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production (never defaulted, handoff §2)' },
|
|
394
407
|
volumeSize: str('Storage size, e.g. "10Gi" (optional)'),
|
|
395
408
|
replicas: int('Read/failover replicas for HA (optional; postgres/CNPG only)'),
|
|
396
409
|
deploymentMode: { type: 'string', description: 'single | ha (optional)' },
|
|
397
410
|
},
|
|
398
|
-
required: ['projectId', 'name', 'engine'],
|
|
411
|
+
required: ['projectId', 'name', 'engine', 'environment'],
|
|
399
412
|
},
|
|
400
413
|
async call(api, args) {
|
|
414
|
+
requireEnv(args.environment);
|
|
401
415
|
const { projectId, ...body } = args;
|
|
402
416
|
const { status, data } = await api.post(`/api/v1/projects/${projectId}/databases`, body);
|
|
403
417
|
return redactSecrets(result(status, data));
|
|
@@ -431,13 +445,14 @@ export const TOOLS = [
|
|
|
431
445
|
projectId: str('Project id'),
|
|
432
446
|
name: str('Queue name'),
|
|
433
447
|
planClass: str('Plan/engine class (optional)'),
|
|
434
|
-
environment:
|
|
448
|
+
environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production (never defaulted, handoff §2)' },
|
|
435
449
|
volumeSize: str('Storage size, e.g. "10Gi" (optional)'),
|
|
436
450
|
dlqEnabled: { type: 'boolean', description: 'Enable a dead-letter queue (optional)' },
|
|
437
451
|
},
|
|
438
|
-
required: ['projectId', 'name'],
|
|
452
|
+
required: ['projectId', 'name', 'environment'],
|
|
439
453
|
},
|
|
440
454
|
async call(api, args) {
|
|
455
|
+
requireEnv(args.environment);
|
|
441
456
|
const { projectId, ...body } = args;
|
|
442
457
|
const { status, data } = await api.post(`/api/v1/projects/${projectId}/queues`, body);
|
|
443
458
|
return redactSecrets(result(status, data));
|
|
@@ -480,11 +495,12 @@ export const TOOLS = [
|
|
|
480
495
|
command: { type: 'array', items: { type: 'string' }, description: 'Command argv (mode=app_command)' },
|
|
481
496
|
url: str('Target URL (mode=http_endpoint)'),
|
|
482
497
|
method: str('HTTP method (mode=http_endpoint), e.g. GET/POST'),
|
|
483
|
-
environment:
|
|
498
|
+
environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production (never defaulted, handoff §2)' },
|
|
484
499
|
},
|
|
485
|
-
required: ['projectId', 'name', 'mode', 'scheduleType'],
|
|
500
|
+
required: ['projectId', 'name', 'mode', 'scheduleType', 'environment'],
|
|
486
501
|
},
|
|
487
502
|
async call(api, args) {
|
|
503
|
+
requireEnv(args.environment);
|
|
488
504
|
const { projectId, ...body } = args;
|
|
489
505
|
const { status, data } = await api.post(`/api/v1/projects/${projectId}/crons`, body);
|
|
490
506
|
return redactSecrets(result(status, data));
|
|
@@ -508,6 +524,16 @@ export const TOOLS = [
|
|
|
508
524
|
return redactSecrets((await api.get(`/api/v1/crons/${args.cronId}`)).data);
|
|
509
525
|
},
|
|
510
526
|
},
|
|
527
|
+
{
|
|
528
|
+
name: 'neo_run_cron',
|
|
529
|
+
description: 'Trigger a scheduled job (cron) to run now, out of schedule (handoff 4.6). Returns the run result. Not applicable to a continuous "seconds" cron.',
|
|
530
|
+
mutating: true,
|
|
531
|
+
schema: { type: 'object', properties: { cronId: str('Cron/scheduled-job id') }, required: ['cronId'] },
|
|
532
|
+
async call(api, args) {
|
|
533
|
+
const { status, data } = await api.post(`/api/v1/crons/${args.cronId}/run`);
|
|
534
|
+
return redactSecrets(result(status, data));
|
|
535
|
+
},
|
|
536
|
+
},
|
|
511
537
|
{
|
|
512
538
|
name: 'neo_create_bucket',
|
|
513
539
|
description: 'Create an S3-compatible object-storage bucket in a project (handoff 4.7). Access keys are provisioned server-side and are NOT returned (write-only). May return pending_approval.',
|
|
@@ -547,6 +573,50 @@ export const TOOLS = [
|
|
|
547
573
|
return redactSecrets((await api.get(`/api/v1/buckets/${args.bucketId}`)).data);
|
|
548
574
|
},
|
|
549
575
|
},
|
|
576
|
+
// --- Typed bindings (handoff 5): attach a DB/queue to a consumer service. The
|
|
577
|
+
// connection string is injected as an env var SERVER-SIDE at the consumer's next
|
|
578
|
+
// deploy (marked a secret) — it is never returned to the agent (write-only). ---
|
|
579
|
+
{
|
|
580
|
+
name: 'neo_bind_resource',
|
|
581
|
+
description:
|
|
582
|
+
'Bind a managed database or queue to a consumer service so its connection is injected as an environment variable at the consumer\'s next deploy — WITHOUT ever exposing the secret to you (handoff 5, write-only). envKey defaults to DATABASE_URL (neo_db) or NATS_URL (neo_queue). After binding, redeploy the consumer (neo_deploy) for it to take effect. The target and consumer must be in the same project.',
|
|
583
|
+
mutating: true,
|
|
584
|
+
schema: {
|
|
585
|
+
type: 'object',
|
|
586
|
+
properties: {
|
|
587
|
+
appId: str('Consumer service/app id that will receive the env var (from neo_list_services)'),
|
|
588
|
+
targetType: { type: 'string', enum: ['neo_db', 'neo_queue'], description: 'neo_db | neo_queue — the resource to attach' },
|
|
589
|
+
targetId: str('The database or queue id to bind'),
|
|
590
|
+
envKey: str('Env var name to inject (optional; default DATABASE_URL for neo_db, NATS_URL for neo_queue)'),
|
|
591
|
+
},
|
|
592
|
+
required: ['appId', 'targetType', 'targetId'],
|
|
593
|
+
},
|
|
594
|
+
async call(api, args) {
|
|
595
|
+
const { appId, ...body } = args;
|
|
596
|
+
const { status, data } = await api.post(`/api/v1/apps/${appId}/bindings`, body);
|
|
597
|
+
return redactSecrets(result(status, data));
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
name: 'neo_list_bindings',
|
|
602
|
+
description: 'List the resource bindings attached to a consumer service — the env keys and target resources, never the secret connection values.',
|
|
603
|
+
mutating: false,
|
|
604
|
+
schema: { type: 'object', properties: { appId: str('Consumer service/app id') }, required: ['appId'] },
|
|
605
|
+
async call(api, args) {
|
|
606
|
+
return redactSecrets((await api.get(`/api/v1/apps/${args.appId}/bindings`)).data);
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
name: 'neo_unbind_resource',
|
|
611
|
+
description: 'Remove a resource binding by id. The env var stops being injected on the consumer\'s next deploy. Removes the wiring only — the database/queue itself is untouched.',
|
|
612
|
+
mutating: true,
|
|
613
|
+
destructive: true,
|
|
614
|
+
schema: { type: 'object', properties: { bindingId: str('Binding id (from neo_list_bindings)') }, required: ['bindingId'] },
|
|
615
|
+
async call(api, args) {
|
|
616
|
+
const { status, data } = await api.del(`/api/v1/bindings/${args.bindingId}`);
|
|
617
|
+
return result(status, data);
|
|
618
|
+
},
|
|
619
|
+
},
|
|
550
620
|
// --- Cleanup (handoff 3.6): MCP can delete every resource type it can reach, so
|
|
551
621
|
// an agent never leaks a resource it created. These are DESTRUCTIVE and permanent;
|
|
552
622
|
// the server hard-deletes the record and tears down the running workload. ---
|