@zibby/mcp-cli 0.3.5 → 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.
- package/index.js +272 -23
- 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
|
-
|
|
351
|
+
toolWithAlias(
|
|
331
352
|
'zibby_list_templates',
|
|
332
|
-
'
|
|
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
|
-
|
|
361
|
+
toolWithAlias(
|
|
340
362
|
'zibby_scaffold_workflow',
|
|
341
|
-
'
|
|
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
|
|
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
|
|
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
|
-
|
|
381
|
+
toolWithAlias(
|
|
359
382
|
'zibby_validate_workflow',
|
|
360
|
-
'
|
|
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('
|
|
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
|
-
|
|
395
|
+
toolWithAlias(
|
|
372
396
|
'zibby_deploy_workflow',
|
|
373
|
-
'
|
|
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
|
|
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().
|
|
380
|
-
|
|
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
|
-
|
|
423
|
+
toolWithAlias(
|
|
397
424
|
'zibby_run_workflow_local',
|
|
398
|
-
'
|
|
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
|
|
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
|
|
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
|
-
|
|
442
|
+
toolWithAlias(
|
|
415
443
|
'zibby_download_workflow',
|
|
416
|
-
'
|
|
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('
|
|
419
|
-
projectId: z.string().min(1).describe('Project the
|
|
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)
|
|
@@ -484,8 +513,18 @@ server.tool(
|
|
|
484
513
|
architecture: z.enum(['x86_64', 'arm64']).optional().describe('CPU architecture for the Fargate task. "arm64" runs on AWS Graviton — ~20% cheaper compute (same price to user). "x86_64" is the historical default + widest catalog compatibility. Omit to accept the catalog tile\'s preferred arch (first entry in its `architectures` array — usually arm64 for tiles that support it).'),
|
|
485
514
|
model: z.string().min(1).optional().describe('Claude model identifier (e.g. claude-sonnet-4-6). Overrides the agent-ops bootstrap default. Use a stronger model (Opus) for complex installs; cheaper (Haiku) for trivial.'),
|
|
486
515
|
anthropicToken: z.string().regex(/^sk-ant-(oat01|api03)-/, 'must be sk-ant-oat01-* or sk-ant-api03-*').optional().describe('Per-deploy Claude credential override. Defaults to the workspace-stored token. SENSITIVE — never log or persist.'),
|
|
516
|
+
maxTurns: z.number().int().min(1).max(200).optional().describe('Claude subprocess max turns. Default 25. Bump for heavy installs (n8n, OpenHands) that need background tool calls + retries.'),
|
|
517
|
+
timeoutMin: z.number().int().min(1).max(120).optional().describe('Bootstrap task wall-clock minutes. Default 30. Bump when npm install / docker pull / native compile dominates wall time.'),
|
|
518
|
+
// Optional Caddy auth sidecar — wraps the public URL with basic-auth
|
|
519
|
+
// or bearer-token validation. Apps like Grafana ship admin/admin and
|
|
520
|
+
// raw n8n/openhands ship with nothing, so gating the public URL is a
|
|
521
|
+
// common ask. Catalog never declares this — it's per-instance.
|
|
522
|
+
authType: z.enum(['basic', 'token', 'none']).optional().describe('Optional reverse-proxy auth in front of the public URL. "basic" = username + password (browser challenge), "token" = Authorization: Bearer header (machine-friendly), "none" = no gate (default, current behavior). Caddy sidecar adds ~30MB image / ~10MB RAM — no tier bump.'),
|
|
523
|
+
authUser: z.string().min(1).max(64).optional().describe('Username for authType=basic. Required when enabling basic. Printable ASCII, no spaces.'),
|
|
524
|
+
authPassword: z.string().min(8).max(256).optional().describe('Password for authType=basic. SENSITIVE — bcrypt-hashed server-side, plaintext never persisted. Required when enabling basic.'),
|
|
525
|
+
authToken: z.string().min(16).max(128).regex(/^[A-Za-z0-9_-]+$/, 'base64url alphabet only').optional().describe('Optional bearer token for authType=token. Omit to let the backend auto-generate a 32-byte token (returned ONCE in the deploy response — surface it to the user with a save-it warning). SENSITIVE.'),
|
|
487
526
|
},
|
|
488
|
-
async ({ appType, goal, projectId, name, provider, architecture, model, anthropicToken }) => {
|
|
527
|
+
async ({ appType, goal, projectId, name, provider, architecture, model, anthropicToken, maxTurns, timeoutMin, authType, authUser, authPassword, authToken }) => {
|
|
489
528
|
// Enforce mutual exclusivity client-side so the agent gets a clear
|
|
490
529
|
// error before we burn an HTTP round-trip. Backend enforces the
|
|
491
530
|
// same invariant (apps.js::deployApp) but this gives a faster
|
|
@@ -510,12 +549,24 @@ server.tool(
|
|
|
510
549
|
if (provider) args.push('--provider', provider);
|
|
511
550
|
if (architecture) args.push('--arch', architecture);
|
|
512
551
|
if (model) args.push('--model', model);
|
|
552
|
+
// --max-turns / --timeout-min are integer caps on the per-instance
|
|
553
|
+
// bootstrap. Both forwarded as argv — neither is sensitive. CLI
|
|
554
|
+
// re-validates the ranges (1..200, 1..120) so a malformed value 400s
|
|
555
|
+
// locally before hitting the backend.
|
|
556
|
+
if (maxTurns !== undefined) args.push('--max-turns', String(maxTurns));
|
|
557
|
+
if (timeoutMin !== undefined) args.push('--timeout-min', String(timeoutMin));
|
|
558
|
+
if (authType) args.push('--auth-type', authType);
|
|
559
|
+
if (authUser) args.push('--auth-user', authUser);
|
|
513
560
|
// anthropicToken is SENSITIVE. Forwarded to the @zibby/cli subprocess
|
|
514
561
|
// via ZIBBY_ANTHROPIC_TOKEN env (NOT argv) so the token never lands
|
|
515
562
|
// in /proc/<pid>/cmdline or argv-scraping ps tools. The CLI sees the
|
|
516
563
|
// env, applies the same regex check, and passes it as a body field.
|
|
564
|
+
// Same treatment for authPassword / authToken — both pulled by the
|
|
565
|
+
// CLI from env so the password / bearer never appears in argv.
|
|
517
566
|
const extraEnv = { ZIBBY_API_KEY: apiKey };
|
|
518
567
|
if (anthropicToken) extraEnv.ZIBBY_ANTHROPIC_TOKEN = anthropicToken;
|
|
568
|
+
if (authPassword) extraEnv.ZIBBY_APP_AUTH_PASSWORD = authPassword;
|
|
569
|
+
if (authToken) extraEnv.ZIBBY_APP_AUTH_TOKEN = authToken;
|
|
519
570
|
return cliResult(await runCli(args, { extraEnv }));
|
|
520
571
|
}
|
|
521
572
|
);
|
|
@@ -676,6 +727,204 @@ server.tool(
|
|
|
676
727
|
}
|
|
677
728
|
);
|
|
678
729
|
|
|
730
|
+
// ── Tool: enable / rotate / disable the Caddy auth sidecar on a running
|
|
731
|
+
// instance. Per-instance opt-in reverse-proxy that gates the public URL
|
|
732
|
+
// with basic-auth (username + bcrypt'd password) or bearer-token
|
|
733
|
+
// validation. Use cases:
|
|
734
|
+
// - User says "lock down my Grafana with admin / hunter2"
|
|
735
|
+
// → authType:'basic', authUser:'admin', authPassword:'hunter2-strong'
|
|
736
|
+
// - User says "give me a token for my n8n public URL"
|
|
737
|
+
// → authType:'token' (backend auto-generates; agent prints once + warns)
|
|
738
|
+
// - User says "rotate the password to <new>" on an existing basic-auth
|
|
739
|
+
// → authPassword:'<new>' (omit authType; PATCH preserves it)
|
|
740
|
+
// - User says "remove auth from my app"
|
|
741
|
+
// → authType:'none'
|
|
742
|
+
server.tool(
|
|
743
|
+
'zibby_set_app_auth',
|
|
744
|
+
'Enable, rotate, or disable the optional Caddy reverse-proxy auth sidecar on a running app instance. The sidecar fronts the public URL with basic-auth or bearer-token validation, useful for apps that ship with weak / no built-in auth (Grafana=admin/admin, raw n8n / openhands = none). PATCH semantics: omitted fields preserve current state, so e.g. passing just authPassword on a basic-auth instance rotates the password without re-specifying the user. For token mode WITHOUT an explicit authToken, the backend generates a fresh 32-byte token and returns it ONCE in the response — surface it to the user with a save-this-now warning since it can never be retrieved again. Triggers a rolling ECS task replace (~30s); EFS data preserved.',
|
|
745
|
+
{
|
|
746
|
+
instanceId: z.string().min(1).describe('Instance ID to update auth on'),
|
|
747
|
+
projectId: z.string().min(1).optional().describe('Project the instance belongs to (picks the right cached API token)'),
|
|
748
|
+
authType: z.enum(['basic', 'token', 'none']).optional().describe('"basic" | "token" | "none". Omit to preserve current type (e.g. rotate just the password). "none" disables the sidecar — public URL exposes the app port directly again.'),
|
|
749
|
+
authUser: z.string().min(1).max(64).optional().describe('Username for basic auth. Required when SWITCHING TO basic; preserved on subsequent rotations.'),
|
|
750
|
+
authPassword: z.string().min(8).max(256).optional().describe('Password for basic auth. SENSITIVE — bcrypt-hashed server-side. Pass to rotate; omit to preserve current.'),
|
|
751
|
+
authToken: z.string().min(16).max(128).regex(/^[A-Za-z0-9_-]+$/, 'base64url alphabet only').optional().describe('Optional explicit bearer token for token auth. Omit on token-mode to let the backend regenerate (returned ONCE — print + warn user). SENSITIVE.'),
|
|
752
|
+
off: z.boolean().optional().describe('Operator-friendly alias for authType:"none". When true, disables the auth sidecar entirely.'),
|
|
753
|
+
},
|
|
754
|
+
async ({ instanceId, projectId, authType, authUser, authPassword, authToken, off }) => {
|
|
755
|
+
const extraEnv = {};
|
|
756
|
+
if (projectId) {
|
|
757
|
+
const apiKey = getProjectApiToken(projectId);
|
|
758
|
+
if (apiKey) extraEnv.ZIBBY_API_KEY = apiKey;
|
|
759
|
+
}
|
|
760
|
+
const args = ['app', 'set-auth', instanceId];
|
|
761
|
+
if (off) args.push('--off');
|
|
762
|
+
if (authType) args.push('--auth-type', authType);
|
|
763
|
+
if (authUser) args.push('--auth-user', authUser);
|
|
764
|
+
// Sensitive args via env (NOT argv) so password / token never lands in
|
|
765
|
+
// /proc/<pid>/cmdline or argv-scraping ps tools — same pattern as
|
|
766
|
+
// anthropicToken on zibby_deploy_app.
|
|
767
|
+
if (authPassword) extraEnv.ZIBBY_APP_AUTH_PASSWORD = authPassword;
|
|
768
|
+
if (authToken) extraEnv.ZIBBY_APP_AUTH_TOKEN = authToken;
|
|
769
|
+
return cliResult(await runCli(args, { extraEnv }));
|
|
770
|
+
}
|
|
771
|
+
);
|
|
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
|
+
|
|
679
928
|
// ── Connect ─────────────────────────────────────────────────────────────
|
|
680
929
|
|
|
681
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.
|
|
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",
|