@zibby/mcp-cli 0.3.7 → 0.3.9

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 +207 -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,19 @@ const server = new McpServer({
193
202
  version: '0.3.0',
194
203
  });
195
204
 
205
+ // ── Agent-named tool registration (product-noun migration: "workflow" → "agent") ──
206
+ // The product UI now calls deployed automations "Agents". Unlike the CLI —
207
+ // where `agent` is an additive ALIAS and `workflow` stays for muscle memory /
208
+ // scripts — MCP tool names are LLM-facing and surfaced wholesale via
209
+ // tools/list. Registering both `*_workflow*` and `*_agent*` would put 6 exact
210
+ // duplicate pairs in front of the model, cluttering the list and muddying tool
211
+ // selection. So MCP exposes ONLY the agent-named tool. The legacy `*_workflow*`
212
+ // name (first arg) is kept in the call signature for traceability but is no
213
+ // longer registered. Internal CLI verbs / API routes / package names unchanged.
214
+ function toolWithAlias(_legacyWorkflowName, agentName, description, schema, handler) {
215
+ server.tool(agentName, description, schema, handler);
216
+ }
217
+
196
218
  // ── Tool: login ─────────────────────────────────────────────────────────
197
219
  // Device-code OAuth flow. Opens the user's browser to the verification URL,
198
220
  // then polls /cli/login/poll until the user authorizes (or it times out).
@@ -327,24 +349,26 @@ server.tool(
327
349
  // ── Tool: list templates ────────────────────────────────────────────────
328
350
  // Local because @zibby/cli reads the bundled template manifest from its
329
351
  // own node_modules (no network call).
330
- server.tool(
352
+ toolWithAlias(
331
353
  '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.',
354
+ 'zibby_list_agent_templates',
355
+ '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
356
  {},
334
357
  async () => cliResult(await runCli(['template', 'list']))
335
358
  );
336
359
 
337
360
  // ── Tool: scaffold workflow ─────────────────────────────────────────────
338
361
  // Writes files to .zibby/workflows/<name>/ in the user's cwd.
339
- server.tool(
362
+ toolWithAlias(
340
363
  '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.',
364
+ 'zibby_scaffold_agent',
365
+ '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
366
  {
343
- name: z.string().min(1).describe('Local workflow folder name (kebab-case)'),
367
+ name: z.string().min(1).describe('Local agent folder name (kebab-case)'),
344
368
  template: z.enum(['browser-test-automation', 'code-analysis', 'generate-test-cases'])
345
369
  .describe('Official template to scaffold from'),
346
370
  skipInstall: z.boolean().optional().default(false)
347
- .describe('Skip running `npm install` in the new workflow folder'),
371
+ .describe('Skip running `npm install` in the new agent folder'),
348
372
  },
349
373
  async ({ name, template, skipInstall }) => {
350
374
  const args = ['g', 'workflow', name, '-t', template, '--no-agent-helpers'];
@@ -355,11 +379,12 @@ server.tool(
355
379
 
356
380
  // ── Tool: validate workflow ─────────────────────────────────────────────
357
381
  // Reads local workflow files + spawns the local validator.
358
- server.tool(
382
+ toolWithAlias(
359
383
  '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.',
384
+ 'zibby_validate_agent',
385
+ '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
386
  {
362
- name: z.string().min(1).describe('Workflow folder name under .zibby/workflows/'),
387
+ name: z.string().min(1).describe('Agent folder name under .zibby/workflows/'),
363
388
  },
364
389
  async ({ name }) => cliResult(await runCli(['workflow', 'validate', name]))
365
390
  );
@@ -368,16 +393,19 @@ server.tool(
368
393
  // Local-essential because the bundling step (zip the .zibby/workflows/<name>/
369
394
  // folder + walk its node_modules) happens on the user's machine before
370
395
  // upload. The remote MCP can't see those files.
371
- server.tool(
396
+ toolWithAlias(
372
397
  '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.',
398
+ 'zibby_deploy_agent',
399
+ '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
400
  {
375
- name: z.string().min(1).describe('Local workflow folder name'),
401
+ name: z.string().min(1).describe('Local agent folder name'),
376
402
  projectId: z.string().min(1).describe('Project to deploy under (use the Zibby Remote MCP\'s zibby_list_projects to discover)'),
377
403
  force: z.boolean().optional().default(false)
378
404
  .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'),
405
+ warm: z.number().int().refine((n) => WARM_POOL_QUANTITY_OPTIONS.includes(n), {
406
+ message: `warm must be one of [${WARM_POOL_QUANTITY_OPTIONS.join(', ')}]`,
407
+ }).optional()
408
+ .describe(`Enable warm-pool execution (${WARM_POOL_QUANTITY_OPTIONS.join('/')} always-on runners) — paid feature, skips ~60s cold start`),
381
409
  },
382
410
  async ({ name, projectId, force, warm }) => {
383
411
  const apiKey = getProjectApiToken(projectId);
@@ -393,13 +421,14 @@ server.tool(
393
421
 
394
422
  // ── Tool: run workflow locally ──────────────────────────────────────────
395
423
  // Spawns a local node process to run the workflow against local files.
396
- server.tool(
424
+ toolWithAlias(
397
425
  '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.',
426
+ 'zibby_run_agent_local',
427
+ '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
428
  {
400
- name: z.string().min(1).describe('Local workflow folder name'),
429
+ name: z.string().min(1).describe('Local agent folder name'),
401
430
  input: z.record(z.string(), z.any()).optional().default({})
402
- .describe('Input params (JSON object passed to the workflow\'s entry node)'),
431
+ .describe('Input params (JSON object passed to the agent\'s entry node)'),
403
432
  },
404
433
  async ({ name, input }) => {
405
434
  const args = ['workflow', 'run', name, '--input', JSON.stringify(input || {})];
@@ -411,12 +440,13 @@ server.tool(
411
440
  // Destructive enough to require explicit user confirmation since it can
412
441
  // overwrite files in the user's working directory. The agent MUST set
413
442
  // `confirm: true` AND provide a `dest` path it has shown to the user.
414
- server.tool(
443
+ toolWithAlias(
415
444
  '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.',
445
+ 'zibby_download_agent',
446
+ '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
447
  {
418
- uuid: z.string().min(1).describe('Workflow UUID to download'),
419
- projectId: z.string().min(1).describe('Project the workflow lives under'),
448
+ uuid: z.string().min(1).describe('Agent UUID to download'),
449
+ projectId: z.string().min(1).describe('Project the agent lives under'),
420
450
  dest: z.string().min(1).describe('Destination directory path (absolute or relative to cwd). Show this to the user before calling.'),
421
451
  confirm: z.literal(true).describe('Must be true. Set only after the user has explicitly approved the download to the specified dest.'),
422
452
  force: z.boolean().optional().default(false)
@@ -741,6 +771,161 @@ server.tool(
741
771
  }
742
772
  );
743
773
 
774
+ // ─── Apps: Solo-mode (dedicated EC2 per app) ──────────────────────────────
775
+ //
776
+ // Five tools mirroring the /apps/solo/* surface. Each is a thin wrapper
777
+ // around the corresponding backend endpoint — see
778
+ // backend/src/handlers/apps-solo.js and the contract at
779
+ // backend/src/handlers/__contracts__/solo-deploy-spec.md.
780
+ //
781
+ // Why these are separate from the cloud-mode tools above:
782
+ // - Different deploy contract (repo + tier + persistence, not appType
783
+ // + goal). Conflating them in one tool overwhelms the LLM with
784
+ // params that only apply to one mode.
785
+ // - Plan/fire is a two-step flow (LLM sees the validation errors
786
+ // from plan, fixes the spec, then fires). The cloud `deploy_app`
787
+ // tool is single-step. Different ergonomics.
788
+ //
789
+ // Tier SSOT lives in backend/src/config/plans.js (SOLO_TIERS). The
790
+ // plan response echoes `pricing.soloTiers` so the LLM can quote the
791
+ // live table to the user; the zod enum below is a compile-time
792
+ // guardrail mirroring the same ids.
793
+
794
+ // Common nested zod schemas — defined once so the plan/fire tools share
795
+ // validation. Treated as the canonical TS shape inline; the contract
796
+ // doc has the authoritative comments.
797
+ const soloSourceSchema = z.union([
798
+ z.object({
799
+ type: z.literal('github'),
800
+ repo: z.string().min(1).describe('GitHub repo in "owner/name" form'),
801
+ ref: z.string().min(1).optional().describe('Git ref (branch, tag, sha). Defaults to repo default branch.'),
802
+ }),
803
+ z.object({
804
+ type: z.literal('tarball'),
805
+ s3Url: z.string().min(1).describe('s3:// URL of the source tarball'),
806
+ }),
807
+ ]);
808
+
809
+ const soloSecretSchema = z.object({
810
+ key: z.string().min(1).describe('Env var name'),
811
+ 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.'),
812
+ });
813
+
814
+ const soloPersistenceSchema = z.object({
815
+ db: z.enum(['sqlite-litestream', 'postgres-walg', 'none']).optional(),
816
+ files: z.enum(['activestorage-s3', 'rclone-bisync', 'none']).optional(),
817
+ });
818
+
819
+ const soloTierEnum = z.enum(['micro', 'small', 'medium', 'large']);
820
+
821
+ // Region SSOT mirror — backend/src/config/plans.js SOLO_REGIONS. The
822
+ // control plane stays in Sydney; this only moves the per-app EC2 + S3 +
823
+ // log group. Pricing is flat across regions (v1). Validated with
824
+ // .refine against the explicit list so an unknown region fails fast
825
+ // LLM-side rather than round-tripping to a backend 400.
826
+ const SOLO_REGION_IDS = ['ap-southeast-2', 'us-east-1', 'us-west-2', 'eu-west-1', 'ap-northeast-1'];
827
+ const soloRegionSchema = z.string()
828
+ .refine((r) => SOLO_REGION_IDS.includes(r), {
829
+ message: `region must be one of ${SOLO_REGION_IDS.join(', ')}`,
830
+ });
831
+
832
+ // Full + partial spec shapes. plan_solo_deploy takes the partial
833
+ // (everything optional) and surfaces missingInputs / ambiguities back
834
+ // to the LLM. fire_solo_deploy demands the full shape.
835
+ const soloPartialSpec = z.object({
836
+ appSlug: z.string().min(1).max(40).optional()
837
+ .describe('lowercase [a-z0-9-]; forms the public hostname <slug>.apps.zibby.dev'),
838
+ source: soloSourceSchema.optional(),
839
+ framework: z.enum(['auto', 'rails', 'node', 'python', 'static', 'other']).optional(),
840
+ tier: soloTierEnum.optional()
841
+ .describe('micro/small/medium/large — see plan response pricing.soloTiers for the live table.'),
842
+ region: soloRegionSchema.optional()
843
+ .describe([
844
+ 'AWS region the VM runs in. One of:',
845
+ ' ap-southeast-2 (Sydney) — Oceania, default/control-plane region',
846
+ ' us-east-1 (N. Virginia) — North America (east), also nearest for South America',
847
+ ' us-west-2 (Oregon) — North America (west)',
848
+ ' eu-west-1 (Ireland) — Europe, also nearest for Africa/Middle East',
849
+ ' ap-northeast-1 (Tokyo) — Asia, nearest for East Asia incl. China/Korea, and the closest option for India/SE Asia',
850
+ 'PICK THE NEAREST region to the user for lowest latency — you already know their',
851
+ 'locale/timezone from this conversation, so infer it (e.g. UTC+10 → Sydney, UTC+8 → Tokyo,',
852
+ 'US Pacific → Oregon, US Eastern/Central → N. Virginia, Europe → Ireland) and pass it',
853
+ 'explicitly rather than defaulting. Only the EC2 + S3 + logs move to this region; the',
854
+ 'Zibby control plane stays in Sydney. Pricing is identical in every region, so choose',
855
+ 'purely on proximity. If you genuinely cannot tell where the user is, omit it (defaults',
856
+ 'to Sydney) or ask them.',
857
+ ].join('\n')),
858
+ secrets: z.array(soloSecretSchema).optional(),
859
+ domain: z.string().optional().describe('Server canonicalizes to <slug>.apps.zibby.dev — value passed here is overridden.'),
860
+ persistence: soloPersistenceSchema.optional(),
861
+ });
862
+
863
+ const soloCompleteSpec = soloPartialSpec.required({
864
+ appSlug: true,
865
+ source: true,
866
+ tier: true,
867
+ });
868
+
869
+ server.tool(
870
+ 'plan_solo_deploy',
871
+ '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.',
872
+ {
873
+ spec: soloPartialSpec.describe('Partial spec — every field is optional. Backend reports back what is missing.'),
874
+ },
875
+ async ({ spec }) => {
876
+ // The MCP doesn't have a project context; we shell out to a generic
877
+ // fetch via the @zibby/cli session token. Future: convert to a
878
+ // dedicated CLI subcommand if this becomes a hot path. For now we
879
+ // dump the spec to a flag the CLI hasn't surfaced yet, falling back
880
+ // to an explicit error so the LLM knows the wiring is partial.
881
+ // The actual call shape parallels fetchJson in commands/app.js.
882
+ return cliResult(await runCli(['app', '_solo-plan', JSON.stringify(spec)]));
883
+ },
884
+ );
885
+
886
+ server.tool(
887
+ 'fire_solo_deploy',
888
+ '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.',
889
+ {
890
+ 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.'),
891
+ },
892
+ async ({ spec }) => {
893
+ return cliResult(await runCli(['app', '_solo-fire', JSON.stringify(spec)]));
894
+ },
895
+ );
896
+
897
+ server.tool(
898
+ 'get_solo_status',
899
+ '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.',
900
+ {
901
+ appSlug: z.string().min(1).describe('App slug returned from fire_solo_deploy'),
902
+ },
903
+ async ({ appSlug }) => {
904
+ return cliResult(await runCli(['app', '_solo-status', appSlug]));
905
+ },
906
+ );
907
+
908
+ server.tool(
909
+ 'list_solo_apps',
910
+ '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.)',
911
+ {},
912
+ async () => {
913
+ return cliResult(await runCli(['app', 'list', '--mode', 'solo']));
914
+ },
915
+ );
916
+
917
+ server.tool(
918
+ 'stop_solo_app',
919
+ '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.',
920
+ {
921
+ appSlug: z.string().min(1),
922
+ confirm: z.literal(true).describe('Must be true. Set only after the user has explicitly approved destroying this instance.'),
923
+ },
924
+ async ({ appSlug }) => {
925
+ return cliResult(await runCli(['app', 'destroy', appSlug, '--yes', '--mode', 'solo']));
926
+ },
927
+ );
928
+
744
929
  // ── Connect ─────────────────────────────────────────────────────────────
745
930
 
746
931
  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.9",
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",