@zibby/mcp-cli 0.3.7 → 0.3.8

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/index.js +206 -22
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -67,6 +67,15 @@ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
67
67
 
68
68
  const API_BASE = process.env.ZIBBY_API_URL || 'https://api-prod.zibby.app';
69
69
 
70
+ // Warm-pool quantity options — MUST stay in sync with
71
+ // backend/src/config/plans.js ADDONS.warm_pool.quantityOptions.
72
+ // The backend rejects anything outside this set with 400. We duplicate
73
+ // the constant here (rather than importing from @zibby/cli) because
74
+ // @zibby/cli has no exports map and the dev-vs-published layouts differ
75
+ // (src/config in dev, dist/config in npm). Mirror at @zibby/cli's
76
+ // src/config/warmPool.js.
77
+ const WARM_POOL_QUANTITY_OPTIONS = [1, 2, 3];
78
+
70
79
  // ── Config file helpers (matches @zibby/cli/src/config/config.js shape) ─
71
80
 
72
81
  function loadConfig() {
@@ -193,6 +202,18 @@ const server = new McpServer({
193
202
  version: '0.3.0',
194
203
  });
195
204
 
205
+ // ── Dual-register helper (product-noun migration: "workflow" → "agent") ──
206
+ // The product UI now calls deployed automations "Agents". We expose every
207
+ // workflow-related tool under BOTH its historical `*_workflow*` name AND a
208
+ // new `*_agent*` name, pointing at the SAME schema + handler. Old names keep
209
+ // working (additive alias, never removed); the agent-named ones are the new
210
+ // primary. Internal CLI verbs / API routes / package names are untouched —
211
+ // only the MCP-facing tool name + description prose change.
212
+ function toolWithAlias(canonicalName, aliasName, description, schema, handler) {
213
+ server.tool(canonicalName, description, schema, handler);
214
+ server.tool(aliasName, description, schema, handler);
215
+ }
216
+
196
217
  // ── Tool: login ─────────────────────────────────────────────────────────
197
218
  // Device-code OAuth flow. Opens the user's browser to the verification URL,
198
219
  // then polls /cli/login/poll until the user authorizes (or it times out).
@@ -327,24 +348,26 @@ server.tool(
327
348
  // ── Tool: list templates ────────────────────────────────────────────────
328
349
  // Local because @zibby/cli reads the bundled template manifest from its
329
350
  // own node_modules (no network call).
330
- server.tool(
351
+ toolWithAlias(
331
352
  'zibby_list_templates',
332
- 'List the official Zibby workflow templates available to scaffold (browser-test-automation, code-analysis, generate-test-cases, etc.). These are the same templates the marketplace deploys. Reads the manifest bundled with the local @zibby/cli — no network call.',
353
+ 'zibby_list_agent_templates',
354
+ 'List the official Zibby agent templates available to scaffold (browser-test-automation, code-analysis, generate-test-cases, etc.). These are the same templates the marketplace deploys. Reads the manifest bundled with the local @zibby/cli — no network call.',
333
355
  {},
334
356
  async () => cliResult(await runCli(['template', 'list']))
335
357
  );
336
358
 
337
359
  // ── Tool: scaffold workflow ─────────────────────────────────────────────
338
360
  // Writes files to .zibby/workflows/<name>/ in the user's cwd.
339
- server.tool(
361
+ toolWithAlias(
340
362
  'zibby_scaffold_workflow',
341
- 'Scaffold a new workflow into the current project\'s .zibby/workflows/<name>/ directory from an official template. Generates graph.mjs, nodes/, state.js, and package.json on the user\'s local disk. Use zibby_list_templates first to see options.',
363
+ 'zibby_scaffold_agent',
364
+ 'Scaffold a new agent into the current project\'s .zibby/workflows/<name>/ directory from an official template. Generates graph.mjs, nodes/, state.js, and package.json on the user\'s local disk. Use zibby_list_agent_templates first to see options.',
342
365
  {
343
- name: z.string().min(1).describe('Local workflow folder name (kebab-case)'),
366
+ name: z.string().min(1).describe('Local agent folder name (kebab-case)'),
344
367
  template: z.enum(['browser-test-automation', 'code-analysis', 'generate-test-cases'])
345
368
  .describe('Official template to scaffold from'),
346
369
  skipInstall: z.boolean().optional().default(false)
347
- .describe('Skip running `npm install` in the new workflow folder'),
370
+ .describe('Skip running `npm install` in the new agent folder'),
348
371
  },
349
372
  async ({ name, template, skipInstall }) => {
350
373
  const args = ['g', 'workflow', name, '-t', template, '--no-agent-helpers'];
@@ -355,11 +378,12 @@ server.tool(
355
378
 
356
379
  // ── Tool: validate workflow ─────────────────────────────────────────────
357
380
  // Reads local workflow files + spawns the local validator.
358
- server.tool(
381
+ toolWithAlias(
359
382
  'zibby_validate_workflow',
360
- 'Static-check a local workflow (.zibby/workflows/<name>/): graph topology, state schema, skill references. Fast (~30ms) — runs entirely locally against files on disk, no API call. Run this before deploy to catch obvious errors.',
383
+ 'zibby_validate_agent',
384
+ 'Static-check a local agent (.zibby/workflows/<name>/): graph topology, state schema, skill references. Fast (~30ms) — runs entirely locally against files on disk, no API call. Run this before deploy to catch obvious errors.',
361
385
  {
362
- name: z.string().min(1).describe('Workflow folder name under .zibby/workflows/'),
386
+ name: z.string().min(1).describe('Agent folder name under .zibby/workflows/'),
363
387
  },
364
388
  async ({ name }) => cliResult(await runCli(['workflow', 'validate', name]))
365
389
  );
@@ -368,16 +392,19 @@ server.tool(
368
392
  // Local-essential because the bundling step (zip the .zibby/workflows/<name>/
369
393
  // folder + walk its node_modules) happens on the user's machine before
370
394
  // upload. The remote MCP can't see those files.
371
- server.tool(
395
+ toolWithAlias(
372
396
  'zibby_deploy_workflow',
373
- 'Deploy a local workflow (.zibby/workflows/<name>/) to Zibby Cloud under the given project. Bundles the local workflow folder + dependencies, then uploads. Returns the workflow UUID + version on success. Use zibby_validate_workflow first to catch errors fast.',
397
+ 'zibby_deploy_agent',
398
+ 'Deploy a local agent (.zibby/workflows/<name>/) to Zibby Cloud under the given project. Bundles the local agent folder + dependencies, then uploads. Returns the agent UUID + version on success. Use zibby_validate_agent first to catch errors fast.',
374
399
  {
375
- name: z.string().min(1).describe('Local workflow folder name'),
400
+ name: z.string().min(1).describe('Local agent folder name'),
376
401
  projectId: z.string().min(1).describe('Project to deploy under (use the Zibby Remote MCP\'s zibby_list_projects to discover)'),
377
402
  force: z.boolean().optional().default(false)
378
403
  .describe('Re-deploy even if source checksum is unchanged'),
379
- warm: z.number().int().min(1).max(5).optional()
380
- .describe('Enable warm-pool execution (1-5 always-on Fargate tasks) — paid feature, skips ~60s cold start'),
404
+ warm: z.number().int().refine((n) => WARM_POOL_QUANTITY_OPTIONS.includes(n), {
405
+ message: `warm must be one of [${WARM_POOL_QUANTITY_OPTIONS.join(', ')}]`,
406
+ }).optional()
407
+ .describe(`Enable warm-pool execution (${WARM_POOL_QUANTITY_OPTIONS.join('/')} always-on runners) — paid feature, skips ~60s cold start`),
381
408
  },
382
409
  async ({ name, projectId, force, warm }) => {
383
410
  const apiKey = getProjectApiToken(projectId);
@@ -393,13 +420,14 @@ server.tool(
393
420
 
394
421
  // ── Tool: run workflow locally ──────────────────────────────────────────
395
422
  // Spawns a local node process to run the workflow against local files.
396
- server.tool(
423
+ toolWithAlias(
397
424
  'zibby_run_workflow_local',
398
- 'Run a local workflow (.zibby/workflows/<name>/) one-shot on the user\'s machine. Does NOT touch the cloud — used for debugging graph.mjs / node code before deploying. Output includes per-node state transitions.',
425
+ 'zibby_run_agent_local',
426
+ 'Run a local agent (.zibby/workflows/<name>/) one-shot on the user\'s machine. Does NOT touch the cloud — used for debugging graph.mjs / node code before deploying. Output includes per-node state transitions.',
399
427
  {
400
- name: z.string().min(1).describe('Local workflow folder name'),
428
+ name: z.string().min(1).describe('Local agent folder name'),
401
429
  input: z.record(z.string(), z.any()).optional().default({})
402
- .describe('Input params (JSON object passed to the workflow\'s entry node)'),
430
+ .describe('Input params (JSON object passed to the agent\'s entry node)'),
403
431
  },
404
432
  async ({ name, input }) => {
405
433
  const args = ['workflow', 'run', name, '--input', JSON.stringify(input || {})];
@@ -411,12 +439,13 @@ server.tool(
411
439
  // Destructive enough to require explicit user confirmation since it can
412
440
  // overwrite files in the user's working directory. The agent MUST set
413
441
  // `confirm: true` AND provide a `dest` path it has shown to the user.
414
- server.tool(
442
+ toolWithAlias(
415
443
  'zibby_download_workflow',
416
- 'Download a deployed workflow back to a local directory (e.g. to edit it then re-deploy). DESTRUCTIVE: can overwrite files in the destination directory. The agent MUST first ask the user for confirmation and the destination path, then call this with confirm=true.',
444
+ 'zibby_download_agent',
445
+ 'Download a deployed agent back to a local directory (e.g. to edit it then re-deploy). DESTRUCTIVE: can overwrite files in the destination directory. The agent MUST first ask the user for confirmation and the destination path, then call this with confirm=true.',
417
446
  {
418
- uuid: z.string().min(1).describe('Workflow UUID to download'),
419
- projectId: z.string().min(1).describe('Project the workflow lives under'),
447
+ uuid: z.string().min(1).describe('Agent UUID to download'),
448
+ projectId: z.string().min(1).describe('Project the agent lives under'),
420
449
  dest: z.string().min(1).describe('Destination directory path (absolute or relative to cwd). Show this to the user before calling.'),
421
450
  confirm: z.literal(true).describe('Must be true. Set only after the user has explicitly approved the download to the specified dest.'),
422
451
  force: z.boolean().optional().default(false)
@@ -741,6 +770,161 @@ server.tool(
741
770
  }
742
771
  );
743
772
 
773
+ // ─── Apps: Solo-mode (dedicated EC2 per app) ──────────────────────────────
774
+ //
775
+ // Five tools mirroring the /apps/solo/* surface. Each is a thin wrapper
776
+ // around the corresponding backend endpoint — see
777
+ // backend/src/handlers/apps-solo.js and the contract at
778
+ // backend/src/handlers/__contracts__/solo-deploy-spec.md.
779
+ //
780
+ // Why these are separate from the cloud-mode tools above:
781
+ // - Different deploy contract (repo + tier + persistence, not appType
782
+ // + goal). Conflating them in one tool overwhelms the LLM with
783
+ // params that only apply to one mode.
784
+ // - Plan/fire is a two-step flow (LLM sees the validation errors
785
+ // from plan, fixes the spec, then fires). The cloud `deploy_app`
786
+ // tool is single-step. Different ergonomics.
787
+ //
788
+ // Tier SSOT lives in backend/src/config/plans.js (SOLO_TIERS). The
789
+ // plan response echoes `pricing.soloTiers` so the LLM can quote the
790
+ // live table to the user; the zod enum below is a compile-time
791
+ // guardrail mirroring the same ids.
792
+
793
+ // Common nested zod schemas — defined once so the plan/fire tools share
794
+ // validation. Treated as the canonical TS shape inline; the contract
795
+ // doc has the authoritative comments.
796
+ const soloSourceSchema = z.union([
797
+ z.object({
798
+ type: z.literal('github'),
799
+ repo: z.string().min(1).describe('GitHub repo in "owner/name" form'),
800
+ ref: z.string().min(1).optional().describe('Git ref (branch, tag, sha). Defaults to repo default branch.'),
801
+ }),
802
+ z.object({
803
+ type: z.literal('tarball'),
804
+ s3Url: z.string().min(1).describe('s3:// URL of the source tarball'),
805
+ }),
806
+ ]);
807
+
808
+ const soloSecretSchema = z.object({
809
+ key: z.string().min(1).describe('Env var name'),
810
+ valueRef: z.string().min(1).optional().describe('Pointer at workspace-credentials (e.g. "workspace:stripe-prod"). Plaintext is NOT accepted — the backend rejects any `value` field.'),
811
+ });
812
+
813
+ const soloPersistenceSchema = z.object({
814
+ db: z.enum(['sqlite-litestream', 'postgres-walg', 'none']).optional(),
815
+ files: z.enum(['activestorage-s3', 'rclone-bisync', 'none']).optional(),
816
+ });
817
+
818
+ const soloTierEnum = z.enum(['micro', 'small', 'medium', 'large']);
819
+
820
+ // Region SSOT mirror — backend/src/config/plans.js SOLO_REGIONS. The
821
+ // control plane stays in Sydney; this only moves the per-app EC2 + S3 +
822
+ // log group. Pricing is flat across regions (v1). Validated with
823
+ // .refine against the explicit list so an unknown region fails fast
824
+ // LLM-side rather than round-tripping to a backend 400.
825
+ const SOLO_REGION_IDS = ['ap-southeast-2', 'us-east-1', 'us-west-2', 'eu-west-1', 'ap-northeast-1'];
826
+ const soloRegionSchema = z.string()
827
+ .refine((r) => SOLO_REGION_IDS.includes(r), {
828
+ message: `region must be one of ${SOLO_REGION_IDS.join(', ')}`,
829
+ });
830
+
831
+ // Full + partial spec shapes. plan_solo_deploy takes the partial
832
+ // (everything optional) and surfaces missingInputs / ambiguities back
833
+ // to the LLM. fire_solo_deploy demands the full shape.
834
+ const soloPartialSpec = z.object({
835
+ appSlug: z.string().min(1).max(40).optional()
836
+ .describe('lowercase [a-z0-9-]; forms the public hostname <slug>.apps.zibby.dev'),
837
+ source: soloSourceSchema.optional(),
838
+ framework: z.enum(['auto', 'rails', 'node', 'python', 'static', 'other']).optional(),
839
+ tier: soloTierEnum.optional()
840
+ .describe('micro/small/medium/large — see plan response pricing.soloTiers for the live table.'),
841
+ region: soloRegionSchema.optional()
842
+ .describe([
843
+ 'AWS region the VM runs in. One of:',
844
+ ' ap-southeast-2 (Sydney) — Oceania, default/control-plane region',
845
+ ' us-east-1 (N. Virginia) — North America (east), also nearest for South America',
846
+ ' us-west-2 (Oregon) — North America (west)',
847
+ ' eu-west-1 (Ireland) — Europe, also nearest for Africa/Middle East',
848
+ ' ap-northeast-1 (Tokyo) — Asia, nearest for East Asia incl. China/Korea, and the closest option for India/SE Asia',
849
+ 'PICK THE NEAREST region to the user for lowest latency — you already know their',
850
+ 'locale/timezone from this conversation, so infer it (e.g. UTC+10 → Sydney, UTC+8 → Tokyo,',
851
+ 'US Pacific → Oregon, US Eastern/Central → N. Virginia, Europe → Ireland) and pass it',
852
+ 'explicitly rather than defaulting. Only the EC2 + S3 + logs move to this region; the',
853
+ 'Zibby control plane stays in Sydney. Pricing is identical in every region, so choose',
854
+ 'purely on proximity. If you genuinely cannot tell where the user is, omit it (defaults',
855
+ 'to Sydney) or ask them.',
856
+ ].join('\n')),
857
+ secrets: z.array(soloSecretSchema).optional(),
858
+ domain: z.string().optional().describe('Server canonicalizes to <slug>.apps.zibby.dev — value passed here is overridden.'),
859
+ persistence: soloPersistenceSchema.optional(),
860
+ });
861
+
862
+ const soloCompleteSpec = soloPartialSpec.required({
863
+ appSlug: true,
864
+ source: true,
865
+ tier: true,
866
+ });
867
+
868
+ server.tool(
869
+ 'plan_solo_deploy',
870
+ 'Validate a (possibly partial) solo-mode deploy spec server-side. Returns one of: (a) `{ok:true, spec, costEstimate, summary, pricing}` when the spec is complete + the user has enough credits — caller can move straight to fire_solo_deploy; (b) `{ok:false, spec, missingInputs:[{field,prompt,suggested?,options?}], ambiguities:[{field,prompt,options}], pricing}` when there are gaps — caller picks one field at a time, asks the user, fills the spec, and re-calls; (c) `{ok:false, ..., insufficientCredits:{balanceMilliCents,neededMilliCents,hint}}` when the spec is complete but the wallet is empty — caller surfaces the top-up hint. NEVER provisions anything — pure validation. `pricing.soloTiers` always echoes the live tier table; render it for the user when they need to pick. Mirrors POST /apps/solo/plan.',
871
+ {
872
+ spec: soloPartialSpec.describe('Partial spec — every field is optional. Backend reports back what is missing.'),
873
+ },
874
+ async ({ spec }) => {
875
+ // The MCP doesn't have a project context; we shell out to a generic
876
+ // fetch via the @zibby/cli session token. Future: convert to a
877
+ // dedicated CLI subcommand if this becomes a hot path. For now we
878
+ // dump the spec to a flag the CLI hasn't surfaced yet, falling back
879
+ // to an explicit error so the LLM knows the wiring is partial.
880
+ // The actual call shape parallels fetchJson in commands/app.js.
881
+ return cliResult(await runCli(['app', '_solo-plan', JSON.stringify(spec)]));
882
+ },
883
+ );
884
+
885
+ server.tool(
886
+ 'fire_solo_deploy',
887
+ 'Launch a solo-mode EC2 instance from a COMPLETE spec. Spec must have appSlug + source + tier filled in — otherwise the server returns 400 with the same missingInputs shape (re-run plan_solo_deploy to fill the gaps). Returns `{appSlug, instanceId, deploymentId, statusUrl, mode:"solo", tier, domain}`. Bills against the user\'s credit balance per backend/src/config/plans.js SOLO_TIERS (monthly flat fee, prorated per minute). On insufficient credits returns 402 — surface the user-friendly top-up hint to the user, do not retry. The instance bootstraps via cloud-init + agent-ops; poll get_solo_status until phase=running (or =failed). Mirrors POST /apps/solo/fire.',
888
+ {
889
+ spec: soloCompleteSpec.describe('Complete SoloDeploySpec — must include appSlug, source, tier. The server re-validates and 400s if anything is missing; it will NOT silently re-run plan.'),
890
+ },
891
+ async ({ spec }) => {
892
+ return cliResult(await runCli(['app', '_solo-fire', JSON.stringify(spec)]));
893
+ },
894
+ );
895
+
896
+ server.tool(
897
+ 'get_solo_status',
898
+ 'Get the current bootstrap/run phase of a solo-mode app instance. Returns `{appSlug, phase, detail}` where phase is one of `provisioning | bootstrapping | deploying | running | failed | unknown`. Use this to poll after fire_solo_deploy — typical happy path: provisioning (~30s) → bootstrapping (~2min) → deploying (~2min) → running. Mirrors GET /apps/solo/{slug}/status.',
899
+ {
900
+ appSlug: z.string().min(1).describe('App slug returned from fire_solo_deploy'),
901
+ },
902
+ async ({ appSlug }) => {
903
+ return cliResult(await runCli(['app', '_solo-status', appSlug]));
904
+ },
905
+ );
906
+
907
+ server.tool(
908
+ 'list_solo_apps',
909
+ 'List the user\'s solo-mode app instances (sibling to zibby_list_apps which lists cloud-mode instances). Returns an array of `{appSlug, instanceId, tier, phase, domain, createdAt}`. Useful when the user asks "show my solo apps" or "what solo instances am I paying for?". (Wired to the same backend listApps endpoint with a mode=solo filter once the row schema lands.)',
910
+ {},
911
+ async () => {
912
+ return cliResult(await runCli(['app', 'list', '--mode', 'solo']));
913
+ },
914
+ );
915
+
916
+ server.tool(
917
+ 'stop_solo_app',
918
+ 'Stop and destroy a solo-mode app instance. DESTRUCTIVE: terminates the EC2 instance, releases the EBS root volume, removes the row. Litestream replica in S3 is PRESERVED — the user can spin up a new instance pointed at the same bucket to restore. The agent MUST first ask the user for explicit confirmation, then call this with confirm=true.',
919
+ {
920
+ appSlug: z.string().min(1),
921
+ confirm: z.literal(true).describe('Must be true. Set only after the user has explicitly approved destroying this instance.'),
922
+ },
923
+ async ({ appSlug }) => {
924
+ return cliResult(await runCli(['app', 'destroy', appSlug, '--yes', '--mode', 'solo']));
925
+ },
926
+ );
927
+
744
928
  // ── Connect ─────────────────────────────────────────────────────────────
745
929
 
746
930
  await server.connect(new StdioServerTransport());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/mcp-cli",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Zibby local-essential MCP Server — local workflow scaffold/validate/run + deploy/download (bundles local files). Pure-API tools live in the Zibby Remote MCP (api-prod.zibby.app/mcp).",
5
5
  "type": "module",
6
6
  "main": "index.js",