@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/mcp/tools.js +81 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biznetgio/neo-app",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "description": "NEO App CLI — one-command agent onboarding, MCP server, and project ops for coding agents (Codex, Claude Code).",
5
5
  "type": "module",
6
6
  "bin": {
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); manual-run/history still TODO (handoff 4.6)' },
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: str('production or staging (optional, default production)'),
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: args.environment || 'production',
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: str('production or staging (optional, default production)'),
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: str('production or staging (optional, default production)'),
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: str('production or staging (optional, default production)'),
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. ---