@zibby/mcp-cli 0.2.1 → 0.3.2

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 (3) hide show
  1. package/README.md +31 -26
  2. package/index.js +232 -308
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,28 +1,26 @@
1
1
  # @zibby/mcp-cli
2
2
 
3
- Zibby's CLI as a Model Context Protocol server. Lets any MCP-aware AI agent — Claude Code, Cursor, OpenAI Codex, Gemini CLI, Continue, Cline, Aider, Goose — deploy, run, debug, and trigger Zibby workflows from chat.
3
+ Zibby's **local-essential** CLI as a Model Context Protocol server. Lets any MCP-aware AI agent — Claude Code, Cursor, OpenAI Codex, Gemini CLI, Continue, Cline, Aider, Goose — log in to Zibby, scaffold/validate/run workflows on local disk, and deploy or download workflows from a chat session.
4
+
5
+ > **Looking for `list_projects`, `trigger_workflow`, `workflow_logs`, marketplace apps, ticket/integration browsing, etc.?** Those tools moved to the **Zibby Remote MCP** at `https://api-prod.zibby.app/mcp` (HTTP transport, 71 tools, no install). Hook your agent up to both — this stdio package for the things that touch your filesystem, the remote MCP for everything else. The remote MCP picks up server-side changes without you having to re-publish or upgrade npm.
4
6
 
5
7
  ## What you get
6
8
 
7
- 13 MCP tools wrapping the same things `@zibby/cli` does:
9
+ 9 MCP tools, every one of them genuinely local:
8
10
 
9
- | Tool | What it does |
11
+ | Tool | Why it has to be local |
10
12
  |---|---|
11
- | `zibby_login` | Opens the user's browser to log in (device-code OAuth). Saves session to `~/.zibby/config.json`. |
12
- | `zibby_logout` | Clears the saved session. |
13
- | `zibby_status` | Shows who's logged in + cached projects + whether the session is still valid. |
14
- | `zibby_list_projects` | Lists the user's Zibby projects. |
15
- | `zibby_list_templates` | Lists official workflow templates (browser-test-automation, code-analysis, generate-test-cases, …). |
16
- | `zibby_scaffold_workflow` | Scaffolds `.zibby/workflows/<name>/` from an official template. |
17
- | `zibby_validate_workflow` | Static-checks a local workflow (~30ms, no API). |
18
- | `zibby_list_workflows` | Lists workflows (local, remote, or both). |
19
- | `zibby_deploy_workflow` | Deploys a local workflow to a project. |
20
- | `zibby_trigger_workflow` | Triggers a deployed workflow by UUID. Returns `jobId`. |
21
- | `zibby_workflow_logs` | Fetches the latest N log lines from a run (one-shot). |
22
- | `zibby_run_workflow_local` | Runs a workflow locally one-shot for debugging — no cloud. |
23
- | `zibby_download_workflow` | Downloads a deployed workflow back to local. Requires explicit user confirmation. |
24
-
25
- Destructive ops (`workflow delete`, `env set/unset`, `schedule set/clear`, `creds`) are **intentionally not exposed**. Manage those from the `zibby` CLI directly.
13
+ | `zibby_login` | Opens the user's browser (device-code OAuth) and writes the session token + cached project API tokens to `~/.zibby/config.json`. |
14
+ | `zibby_logout` | Clears the saved session from `~/.zibby/config.json`. |
15
+ | `zibby_status` | Reads `~/.zibby/config.json` to show which login is active on **this machine**. (The remote MCP also has a `zibby_status`, but only this one sees the local file.) |
16
+ | `zibby_list_templates` | Lists workflow templates bundled with the local `@zibby/cli` — no network. |
17
+ | `zibby_scaffold_workflow` | Writes `.zibby/workflows/<name>/` files to local disk. |
18
+ | `zibby_validate_workflow` | Reads local workflow files + runs the validator (~30ms, no API). |
19
+ | `zibby_run_workflow_local` | Spawns a local node process to run a workflow against local data. |
20
+ | `zibby_deploy_workflow` | Bundles the local workflow folder + dependencies, then uploads. Bundling is local. |
21
+ | `zibby_download_workflow` | Downloads a deployed workflow back to local disk. Requires explicit user confirmation. |
22
+
23
+ Destructive ops (`workflow delete`, `env set/unset`, `schedule set/clear`, `creds`) are **intentionally not exposed**. Manage those from the `zibby` CLI directly, or via the Zibby Remote MCP where they exist.
26
24
 
27
25
  ## Prerequisites
28
26
 
@@ -43,6 +41,10 @@ That's it. No global `npm install` needed — `npx` handles the bundle (which in
43
41
  "zibby": {
44
42
  "command": "npx",
45
43
  "args": ["-y", "@zibby/mcp-cli"]
44
+ },
45
+ "zibby-remote": {
46
+ "type": "http",
47
+ "url": "https://api-prod.zibby.app/mcp"
46
48
  }
47
49
  }
48
50
  }
@@ -125,7 +127,9 @@ If your agent on Windows can't find `npx`, wrap it in `cmd /c`:
125
127
  Two-stage by design (mirrors how the `zibby` CLI works):
126
128
 
127
129
  1. **Session token** (`zibby_login`) — device-code OAuth via browser. Identifies the user.
128
- 2. **Per-project API tokens** — fetched by `zibby_login` / `zibby_list_projects` and cached locally. The MCP server picks the right token automatically when you call a project-scoped tool like `zibby_deploy_workflow`.
130
+ 2. **Per-project API tokens** — fetched by `zibby_login` and cached locally in `~/.zibby/config.json`. The MCP server picks the right token automatically when you call a project-scoped tool like `zibby_deploy_workflow` or `zibby_download_workflow`.
131
+
132
+ If you grant your account access to a new project, re-run `zibby_login` (or `zibby_logout` + `zibby_login`) to refresh the cached project list.
129
133
 
130
134
  All credentials live in `~/.zibby/config.json` (mode `0600`). The MCP server reads/writes that file directly — no separate credential store.
131
135
 
@@ -135,16 +139,16 @@ The user's password never touches the MCP server: login is OAuth in the browser,
135
139
 
136
140
  ```
137
141
  User: Deploy the browser-test template to my "playhouse" project.
138
- Agent: → zibby_list_projects (finds projectId)
139
- → zibby_scaffold_workflow (browser-test-automation → .zibby/workflows/playhouse-tests/)
140
- → zibby_validate_workflow (catches obvious errors)
141
- → zibby_deploy_workflow (returns UUID + version)
142
+ Agent: → (via Remote MCP) zibby_list_projects (finds projectId)
143
+ → zibby_scaffold_workflow (browser-test-automation → .zibby/workflows/playhouse-tests/)
144
+ → zibby_validate_workflow (catches obvious errors locally)
145
+ → zibby_deploy_workflow (bundles local folder, uploads, returns UUID + version)
142
146
  → "Deployed v1 of playhouse-tests. UUID 988…"
143
147
 
144
148
  User: Run it against staging.zibby.dev.
145
- Agent: → zibby_trigger_workflow (input: { url: "https://staging.zibby.dev" })
149
+ Agent: → (via Remote MCP) zibby_trigger_workflow (input: { url: "https://staging.zibby.dev" })
146
150
  → returns { jobId: "abc-123" }
147
- zibby_workflow_logs (lines: 200, jobId: "abc-123")
151
+ → (via Remote MCP) zibby_get_workflow_job_logs (jobId: "abc-123")
148
152
  → "Run completed. Found 0 errors."
149
153
  ```
150
154
 
@@ -153,10 +157,11 @@ Agent: → zibby_trigger_workflow (input: { url: "https://staging.zibby
153
157
  | Problem | Likely cause |
154
158
  |---|---|
155
159
  | `Not logged in` on every call | `~/.zibby/config.json` missing or corrupted. Call `zibby_login`. |
156
- | `No API token cached for project` | Project list out of date. Call `zibby_list_projects` to refresh. |
160
+ | `No API token cached for project` | Project list out of date. Re-run `zibby_login` (or `zibby_logout` + `zibby_login`) to refresh. |
157
161
  | Tool returns text with ANSI color codes | Some agent UIs don't strip them. We set `NO_COLOR=1` already; if you still see them, your agent's display is the issue. |
158
162
  | `npx -y` hangs on first install | First-time download. Subsequent invocations are cached. |
159
163
  | Tool times out on long deploys | The wrapped CLI command exceeded 10 min. Re-run from a terminal with `zibby workflow deploy` to see live output. |
164
+ | Wanting `list_projects` / `trigger_workflow` / `workflow_logs` | Those live in the Zibby Remote MCP. Add it as a second `mcpServers` entry (`https://api-prod.zibby.app/mcp`). |
160
165
 
161
166
  ## Security model
162
167
 
package/index.js CHANGED
@@ -1,21 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  /* global process */
3
3
  /**
4
- * Zibby CLI MCP Server
4
+ * Zibby CLI MCP Server (local-essential tools only)
5
5
  *
6
- * Exposes the @zibby/cli surface (workflow deploy / run / trigger / logs / …)
7
- * as MCP tools so AI agents (Claude Code, Cursor, OpenAI Codex, Gemini, …)
8
- * can drive Zibby workflows from chat.
6
+ * This stdio MCP server exposes the @zibby/cli surface that genuinely needs
7
+ * to run on the user's local machine login (OAuth browser flow), local
8
+ * workflow scaffolding/validate/run, deploy (bundles local files), and
9
+ * download (writes files locally).
10
+ *
11
+ * Every pure-API tool (list projects, list/trigger workflows, fetch logs,
12
+ * marketplace apps, etc.) is served by the Zibby Remote MCP at
13
+ * https://api-prod.zibby.app/mcp — point your agent there for those. The
14
+ * remote MCP picks up server-side changes without an npm publish/upgrade,
15
+ * so we deliberately do NOT duplicate that surface here.
9
16
  *
10
17
  * Distribution model: stdio MCP server published to npm. Spawned by the
11
18
  * agent's host process via `npx -y @zibby/mcp-cli`. The agent's stdin/stdout
12
- * is the MCP transport. All HTTP API traffic goes user-machine api-prod.zibby.app,
13
- * authenticated with a project-scoped API token read from ~/.zibby/config.json
14
- * (the same file `zibby login` writes).
19
+ * is the MCP transport. All HTTP traffic for the OAuth flow goes
20
+ * user-machine api-prod.zibby.app. Project-scoped API tokens are cached
21
+ * in ~/.zibby/config.json (the same file `zibby login` writes).
15
22
  *
16
- * Implementation: shell-out to the bundled @zibby/cli binary. We resolve the
17
- * CLI through our own node_modules so the version is pinned via package.json
18
- * (independent of any global PATH install).
23
+ * Implementation: shell-out to the bundled @zibby/cli binary for the local
24
+ * file-touching commands. We resolve the CLI through our own node_modules
25
+ * so the version is pinned via package.json (independent of any global
26
+ * PATH install).
19
27
  *
20
28
  * Auth: login is implemented in-process via the device-code OAuth flow
21
29
  * against api-prod.zibby.app/cli/login/{initiate,poll}. We mirror the file
@@ -38,10 +46,19 @@ const require = createRequire(import.meta.url);
38
46
 
39
47
  // ── Constants ───────────────────────────────────────────────────────────
40
48
 
41
- // Pinned via our package.json's @zibby/cli dependency. Using
42
- // require.resolve so we always pick the CLI in our own node_modules,
43
- // not whichever version a user happens to have installed globally.
44
- const ZIBBY_BIN = require.resolve('@zibby/cli/bin/zibby.js');
49
+ // Resolve @zibby/cli's bin path via ITS package.json "bin" field (not a
50
+ // hard-coded subpath). Dev workspace has bin/zibby.js at the top level;
51
+ // the npm-published package ships dist/bin/zibby.js so the literal
52
+ // `@zibby/cli/bin/zibby.js` subpath only works in dev and throws
53
+ // MODULE_NOT_FOUND under npx. Reading package.json + dirname + bin entry
54
+ // works in both layouts.
55
+ const ZIBBY_BIN = (() => {
56
+ const pkgPath = require.resolve('@zibby/cli/package.json');
57
+ const pkg = require(pkgPath);
58
+ const binEntry = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.zibby || pkg.bin?.zb;
59
+ if (!binEntry) throw new Error('@zibby/cli package.json: missing "bin" field');
60
+ return join(pkgPath.replace(/[/\\]package\.json$/, ''), binEntry);
61
+ })();
45
62
 
46
63
  // Mirrors @zibby/cli's config storage. We read/write the same file so the
47
64
  // CLI commands we shell out to pick up our login state, and vice versa.
@@ -83,7 +100,8 @@ function clearSession() {
83
100
  /**
84
101
  * Look up a project's per-project API token (project-scoped, what workflow
85
102
  * commands need). Returns null when the project isn't in the saved list
86
- * (caller should prompt the user to log in / re-run zibby_list_projects).
103
+ * (caller should prompt the user to re-run zibby_login to refresh the
104
+ * cached project list).
87
105
  */
88
106
  function getProjectApiToken(projectId) {
89
107
  return getProjects().find((p) => p.projectId === projectId)?.apiToken || null;
@@ -172,7 +190,7 @@ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
172
190
 
173
191
  const server = new McpServer({
174
192
  name: 'zibby-cli',
175
- version: '0.1.0',
193
+ version: '0.3.0',
176
194
  });
177
195
 
178
196
  // ── Tool: login ─────────────────────────────────────────────────────────
@@ -181,7 +199,7 @@ const server = new McpServer({
181
199
  // Writes session + projects to ~/.zibby/config.json on success.
182
200
  server.tool(
183
201
  'zibby_login',
184
- 'Log in to Zibby. Opens the user\'s browser to the Zibby login page. The user authorizes in the browser; this tool polls until the auth completes and saves the session to ~/.zibby/config.json. Subsequent tool calls in the same MCP session use the saved credentials automatically.',
202
+ 'Log in to Zibby. Opens the user\'s browser to the Zibby login page. The user authorizes in the browser; this tool polls until the auth completes and saves the session + project API tokens to ~/.zibby/config.json. Subsequent local tool calls (deploy, download) use the saved credentials automatically. Re-run to refresh the cached project list after granting access to a new project.',
185
203
  {},
186
204
  async () => {
187
205
  // If already logged in, short-circuit — agent shouldn't trigger a
@@ -230,7 +248,7 @@ server.tool(
230
248
  if (result.status === 'authorized') {
231
249
  // Fetch projects to seed the saved list (workflow ops need per-
232
250
  // project apiTokens). Best-effort — if it fails we still save the
233
- // session token and the user can rerun zibby_list_projects.
251
+ // session token; the user can re-run zibby_login to refresh.
234
252
  let projects = [];
235
253
  try {
236
254
  const pRes = await fetch(`${API_BASE}/projects`, {
@@ -276,9 +294,13 @@ server.tool(
276
294
  );
277
295
 
278
296
  // ── Tool: status ────────────────────────────────────────────────────────
297
+ // Local-only inspector for which session is active on this machine.
298
+ // (There is also a remote zibby_status served by the Zibby Remote MCP,
299
+ // but only this one can see the local file state — i.e. which login the
300
+ // shelled-out CLI commands will use.)
279
301
  server.tool(
280
302
  'zibby_status',
281
- 'Show current login status: who is logged in, how many projects are cached locally, and whether the session token is still valid against the Zibby API.',
303
+ 'Show the local Zibby session info from ~/.zibby/config.json: who is logged in, how many projects are cached locally for shell-out commands (deploy/download), and whether the session token is still valid against the Zibby API. Use this to confirm WHICH account/session the local tools (deploy/download/scaffold) will use.',
282
304
  {},
283
305
  async () => {
284
306
  const token = getSessionToken();
@@ -302,44 +324,21 @@ server.tool(
302
324
  }
303
325
  );
304
326
 
305
- // ── Tool: list projects ─────────────────────────────────────────────────
306
- server.tool(
307
- 'zibby_list_projects',
308
- 'List the Zibby projects the logged-in user has access to. Returns {projectId, name} pairs. Use the projectId in subsequent workflow tool calls.',
309
- {},
310
- async () => {
311
- const token = getSessionToken();
312
- if (!token) return { isError: true, content: [{ type: 'text', text: 'Not logged in. Call zibby_login first.' }] };
313
- // Pull fresh from API and refresh the local cache while we're at it.
314
- const res = await fetch(`${API_BASE}/projects`, {
315
- headers: { Authorization: `Bearer ${token}` },
316
- });
317
- if (!res.ok) {
318
- return { isError: true, content: [{ type: 'text', text: `Failed to list projects: ${res.status}` }] };
319
- }
320
- const data = await res.json();
321
- const projects = (data.projects || []).map((p) => ({
322
- projectId: p.projectId,
323
- name: p.name,
324
- apiToken: p.apiToken, // saved locally for shell-out, not surfaced below
325
- }));
326
- saveConfig({ ...loadConfig(), projects });
327
- return jsonResult(projects.map(({ projectId, name }) => ({ projectId, name })));
328
- }
329
- );
330
-
331
327
  // ── Tool: list templates ────────────────────────────────────────────────
328
+ // Local because @zibby/cli reads the bundled template manifest from its
329
+ // own node_modules (no network call).
332
330
  server.tool(
333
331
  'zibby_list_templates',
334
- '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.',
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.',
335
333
  {},
336
334
  async () => cliResult(await runCli(['template', 'list']))
337
335
  );
338
336
 
339
337
  // ── Tool: scaffold workflow ─────────────────────────────────────────────
338
+ // Writes files to .zibby/workflows/<name>/ in the user's cwd.
340
339
  server.tool(
341
340
  'zibby_scaffold_workflow',
342
- '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. Use zibby_list_templates first to see options.',
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.',
343
342
  {
344
343
  name: z.string().min(1).describe('Local workflow folder name (kebab-case)'),
345
344
  template: z.enum(['browser-test-automation', 'code-analysis', 'generate-test-cases'])
@@ -355,43 +354,26 @@ server.tool(
355
354
  );
356
355
 
357
356
  // ── Tool: validate workflow ─────────────────────────────────────────────
357
+ // Reads local workflow files + spawns the local validator.
358
358
  server.tool(
359
359
  'zibby_validate_workflow',
360
- 'Static-check a local workflow (.zibby/workflows/<name>/): graph topology, state schema, skill references. Fast (~30ms) — runs entirely locally, no API call. Run this before deploy to catch obvious errors.',
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.',
361
361
  {
362
362
  name: z.string().min(1).describe('Workflow folder name under .zibby/workflows/'),
363
363
  },
364
364
  async ({ name }) => cliResult(await runCli(['workflow', 'validate', name]))
365
365
  );
366
366
 
367
- // ── Tool: list workflows ────────────────────────────────────────────────
368
- server.tool(
369
- 'zibby_list_workflows',
370
- 'List workflows. Defaults to interleaving local (.zibby/workflows/) and remote (deployed to a project). Pass projectId to filter remote to a specific project.',
371
- {
372
- projectId: z.string().optional().describe('Optional — limit remote results to this project'),
373
- scope: z.enum(['all', 'local', 'remote']).optional().default('all')
374
- .describe('all = local + remote (default), local = only local files, remote = only deployed'),
375
- },
376
- async ({ projectId, scope }) => {
377
- const args = ['workflow', 'list'];
378
- if (scope === 'local') args.push('--local-only');
379
- if (scope === 'remote') args.push('--remote-only');
380
- if (projectId) args.push('--project', projectId);
381
- const apiKey = projectId ? getProjectApiToken(projectId) : null;
382
- return cliResult(await runCli(args, {
383
- extraEnv: apiKey ? { ZIBBY_API_KEY: apiKey } : {},
384
- }));
385
- }
386
- );
387
-
388
367
  // ── Tool: deploy workflow ───────────────────────────────────────────────
368
+ // Local-essential because the bundling step (zip the .zibby/workflows/<name>/
369
+ // folder + walk its node_modules) happens on the user's machine before
370
+ // upload. The remote MCP can't see those files.
389
371
  server.tool(
390
372
  'zibby_deploy_workflow',
391
- 'Deploy a local workflow (.zibby/workflows/<name>/) to Zibby Cloud under the given project. Returns the workflow UUID + version on success. Use zibby_validate_workflow first to catch errors fast.',
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.',
392
374
  {
393
375
  name: z.string().min(1).describe('Local workflow folder name'),
394
- projectId: z.string().min(1).describe('Project to deploy under (see zibby_list_projects)'),
376
+ projectId: z.string().min(1).describe('Project to deploy under (use the Zibby Remote MCP\'s zibby_list_projects to discover)'),
395
377
  force: z.boolean().optional().default(false)
396
378
  .describe('Re-deploy even if source checksum is unchanged'),
397
379
  warm: z.number().int().min(1).max(5).optional()
@@ -400,7 +382,7 @@ server.tool(
400
382
  async ({ name, projectId, force, warm }) => {
401
383
  const apiKey = getProjectApiToken(projectId);
402
384
  if (!apiKey) {
403
- return { isError: true, content: [{ type: 'text', text: `No API token cached for project ${projectId}. Run zibby_list_projects to refresh, or zibby_login if not logged in.` }] };
385
+ return { isError: true, content: [{ type: 'text', text: `No API token cached for project ${projectId}. Run zibby_login (or zibby_logout + zibby_login) to refresh the local project list.` }] };
404
386
  }
405
387
  const args = ['workflow', 'deploy', name, '--project', projectId];
406
388
  if (force) args.push('--force');
@@ -409,64 +391,15 @@ server.tool(
409
391
  }
410
392
  );
411
393
 
412
- // ── Tool: trigger workflow ──────────────────────────────────────────────
413
- server.tool(
414
- 'zibby_trigger_workflow',
415
- 'Trigger an already-deployed workflow by UUID. Pass input params as a JSON object. Returns the jobId — pass to zibby_workflow_logs to read execution output.',
416
- {
417
- uuid: z.string().min(1).describe('Workflow UUID (from zibby_list_workflows or zibby_deploy_workflow output)'),
418
- projectId: z.string().min(1).describe('Project the workflow lives under'),
419
- input: z.record(z.string(), z.any()).optional().default({})
420
- .describe('Input params for the workflow — shape depends on the workflow\'s state schema'),
421
- idempotencyKey: z.string().optional()
422
- .describe('Optional idempotency key — same key + same input = same job, no duplicate execution'),
423
- },
424
- async ({ uuid, projectId, input, idempotencyKey }) => {
425
- const apiKey = getProjectApiToken(projectId);
426
- if (!apiKey) {
427
- return { isError: true, content: [{ type: 'text', text: `No API token cached for project ${projectId}. Run zibby_list_projects.` }] };
428
- }
429
- const args = ['workflow', 'trigger', uuid, '--project', projectId, '--input', JSON.stringify(input || {})];
430
- if (idempotencyKey) args.push('--idempotency-key', idempotencyKey);
431
- return cliResult(await runCli(args, { extraEnv: { ZIBBY_API_KEY: apiKey } }));
432
- }
433
- );
434
-
435
- // ── Tool: workflow logs ─────────────────────────────────────────────────
436
- server.tool(
437
- 'zibby_workflow_logs',
438
- 'Fetch the most recent N log lines from a workflow execution. One-shot — does NOT stream. For long-running runs, call repeatedly. Either jobId OR workflowName is required.',
439
- {
440
- projectId: z.string().min(1).describe('Project the run lives under'),
441
- jobId: z.string().optional().describe('Specific job to fetch logs for (returned by zibby_trigger_workflow)'),
442
- workflowName: z.string().optional().describe('Alternative to jobId — fetches the latest run for this workflow name'),
443
- lines: z.number().int().min(1).max(5000).optional().default(500)
444
- .describe('Max log lines to fetch'),
445
- },
446
- async ({ projectId, jobId, workflowName, lines }) => {
447
- if (!jobId && !workflowName) {
448
- return { isError: true, content: [{ type: 'text', text: 'Either jobId or workflowName is required.' }] };
449
- }
450
- const apiKey = getProjectApiToken(projectId);
451
- if (!apiKey) {
452
- return { isError: true, content: [{ type: 'text', text: `No API token cached for project ${projectId}. Run zibby_list_projects.` }] };
453
- }
454
- const args = ['workflow', 'logs'];
455
- if (jobId) args.push(jobId);
456
- args.push('--project', projectId, '--lines', String(lines));
457
- if (workflowName) args.push('--workflow', workflowName);
458
- return cliResult(await runCli(args, { extraEnv: { ZIBBY_API_KEY: apiKey } }));
459
- }
460
- );
461
-
462
394
  // ── Tool: run workflow locally ──────────────────────────────────────────
395
+ // Spawns a local node process to run the workflow against local files.
463
396
  server.tool(
464
397
  'zibby_run_workflow_local',
465
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.',
466
399
  {
467
400
  name: z.string().min(1).describe('Local workflow folder name'),
468
401
  input: z.record(z.string(), z.any()).optional().default({})
469
- .describe('Input params (same shape as zibby_trigger_workflow)'),
402
+ .describe('Input params (JSON object passed to the workflow\'s entry node)'),
470
403
  },
471
404
  async ({ name, input }) => {
472
405
  const args = ['workflow', 'run', name, '--input', JSON.stringify(input || {})];
@@ -492,7 +425,7 @@ server.tool(
492
425
  async ({ uuid, projectId, dest, force }) => {
493
426
  const apiKey = getProjectApiToken(projectId);
494
427
  if (!apiKey) {
495
- return { isError: true, content: [{ type: 'text', text: `No API token cached for project ${projectId}. Run zibby_list_projects.` }] };
428
+ return { isError: true, content: [{ type: 'text', text: `No API token cached for project ${projectId}. Run zibby_login (or zibby_logout + zibby_login) to refresh the local project list.` }] };
496
429
  }
497
430
  const args = ['workflow', 'download', uuid, '--dest', dest];
498
431
  if (force) args.push('--force');
@@ -500,243 +433,234 @@ server.tool(
500
433
  }
501
434
  );
502
435
 
503
- // ─────────────────────────────────────────────────────────────────────────
504
- // agent-ops integration tools
505
- //
506
- // These wrap the (currently in development) Zibby control-plane endpoints
507
- // for deploying / managing hosted app instances. Each instance runs the
508
- // open-source agent-ops daemon (github.com/ZibbyHQ/agent-ops) as a sidecar
509
- // — the daemon's own MCP server is what the user's local agent talks to
510
- // for per-instance operations. The tools here cover the cross-instance
511
- // surface that lives in Zibby's control plane.
512
- //
513
- // Backend endpoints these wrap are still being built; the MCP tools are
514
- // shipped now so client-side wiring lands first. Each tool will surface a
515
- // clear "control plane not yet deployed" error until the backend ships.
516
-
517
- const APPS_API_BASE = `${API_BASE}/apps`;
518
-
519
- async function callAppsAPI(path, opts = {}) {
520
- // /catalog and /catalog/{appType} are public — let unauthenticated
521
- // calls through (server enforces auth on the rest). Auth-required
522
- // endpoints return 401 from the backend and we surface that to the
523
- // tool caller; no need to gate here.
524
- const token = getSessionToken();
525
- const url = `${APPS_API_BASE}${path}`;
526
- const init = {
527
- method: opts.method || 'GET',
528
- headers: {
529
- 'Content-Type': 'application/json',
530
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
531
- ...(opts.headers || {}),
532
- },
533
- };
534
- if (opts.body) init.body = JSON.stringify(opts.body);
535
- try {
536
- const res = await fetch(url, init);
537
- const text = await res.text().catch(() => '');
538
- let body = null;
539
- try { body = text ? JSON.parse(text) : null; } catch { body = text; }
540
- return { status: res.status, body, ok: res.ok };
541
- } catch (err) {
542
- return { error: err.message, status: 0 };
543
- }
544
- }
545
-
546
- // ── Tool: deploy app ────────────────────────────────────────────────────
547
- server.tool(
548
- 'zibby_deploy_app',
549
- 'Deploy a marketplace app (n8n, Grafana, Open WebUI, …) into a Zibby project. ' +
550
- 'Returns 202 + the instanceId + the public URL. Once provisioning finishes the URL serves the app; ' +
551
- 'use the zibby_app_* tools to interact with its ops agent. No second MCP install needed — every ' +
552
- 'app tool proxies through the Zibby backend.',
553
- {
554
- appType: z.string().min(1).describe('Catalog id (use zibby_list_app_catalog to see options, e.g. "n8n")'),
555
- projectId: z.string().min(1).describe('Zibby project to deploy under'),
556
- name: z.string().optional().describe('Optional display name for the instance'),
557
- },
558
- async ({ appType, projectId, name }) => {
559
- const res = await callAppsAPI('/deploy', {
560
- method: 'POST',
561
- body: { appType, projectId, name },
562
- });
563
- if (res.error) return { isError: true, content: [{ type: 'text', text: `Network: ${res.error}` }] };
564
- if (!res.ok) {
565
- const detail = typeof res.body === 'string' ? res.body : JSON.stringify(res.body || {});
566
- return { isError: true, content: [{ type: 'text', text: `Deploy failed (${res.status}): ${detail}` }] };
567
- }
568
- return jsonResult(res.body);
569
- }
570
- );
436
+ // ─── Apps: Managed App instances (hosted n8n / grafana / gas-town etc) ─
437
+ // Five tools mirroring the workflow surface above. Each shells out to
438
+ // `zibby app …` and surfaces the CLI's text output to the agent.
571
439
 
572
- // ── Tool: list marketplace catalog ──────────────────────────────────────
440
+ // ── Tool: list app templates (the catalog). Public endpoint — no auth.
573
441
  server.tool(
574
- 'zibby_list_app_catalog',
575
- 'List every app available in the Zibby marketplace. Returns tile metadata (appType, displayName, ' +
576
- 'tagline, category, version, ratingAvg, installCount). Public — no Zibby login required.',
442
+ 'zibby_list_app_templates',
443
+ 'List the official Zibby app catalog (n8n, grafana, gas-town, open-webui, …). These are the same apps the marketplace deploys. No auth required — anyone can browse the catalog before signing in. Each entry exposes an `architectures` array listing the CPU arches it supports (x86_64 and/or arm64); pass the first entry to zibby_deploy_app\'s `architecture` arg to honor operator preference, or omit it entirely to let the server pick.',
577
444
  {},
578
- async () => {
579
- const res = await callAppsAPI('/catalog');
580
- if (res.error) return { isError: true, content: [{ type: 'text', text: `Network: ${res.error}` }] };
581
- if (!res.ok) return { isError: true, content: [{ type: 'text', text: `Catalog failed (${res.status})` }] };
582
- return jsonResult(res.body);
583
- }
445
+ async () => cliResult(await runCli(['app', 'templates']))
584
446
  );
585
447
 
586
- // ── Tool: list apps ──────────────────────────────────────────────────────
448
+ // ── Tool: list deployed apps. Project-scoped when projectId is given;
449
+ // otherwise lists every instance under the caller's account.
587
450
  server.tool(
588
451
  'zibby_list_apps',
589
- 'List hosted app instances in a project. Returns each instance\'s id, template, status, and MCP endpoint URL.',
452
+ 'List the user\'s deployed app instances (live status, app type/version, project). Pass projectId to scope to one project, otherwise returns everything under the account.',
590
453
  {
591
- projectId: z.string().min(1).describe('Zibby project to scope to'),
454
+ projectId: z.string().min(1).optional()
455
+ .describe('Optional project ID — filter to one project\'s instances'),
592
456
  },
593
457
  async ({ projectId }) => {
594
- const res = await callAppsAPI(`?projectId=${encodeURIComponent(projectId)}`);
595
- if (res.error) return { isError: true, content: [{ type: 'text', text: `Network: ${res.error}` }] };
596
- if (!res.ok) return { isError: true, content: [{ type: 'text', text: `List failed (${res.status})` }] };
597
- return jsonResult(res.body);
458
+ const args = ['app', 'list'];
459
+ if (projectId) args.push('--project', projectId);
460
+ // Use the project API token when a project is supplied (matches the
461
+ // workflow tools' pattern); otherwise let the CLI fall back to the
462
+ // session token from ~/.zibby/config.json.
463
+ const extraEnv = {};
464
+ if (projectId) {
465
+ const apiKey = getProjectApiToken(projectId);
466
+ if (apiKey) extraEnv.ZIBBY_API_KEY = apiKey;
467
+ }
468
+ return cliResult(await runCli(args, { extraEnv }));
598
469
  }
599
470
  );
600
471
 
601
- // ── Tool: app status ─────────────────────────────────────────────────────
472
+ // ── Tool: deploy an app. Picks a catalog entry and provisions a new
473
+ // hosted instance under the target project. The CLI's interactive
474
+ // project picker is skipped here because we require projectId up front.
602
475
  server.tool(
603
- 'zibby_app_status',
604
- 'Show detailed status of one hosted app instance: task state, sidecar version, agent-ops heartbeat, last incident.',
476
+ 'zibby_deploy_app',
477
+ 'Deploy an app from the catalog (appType) OR a custom install described in natural language (goal). Catalog is curated by Zibby and license-safe to host; goal-based deploys are user-directed installs where the user (not Zibby) chooses what to install. License-sensitive apps like n8n (SUL) should ONLY be deployed via goal at user\'s direction, never via catalog. Provisions a new hosted instance (ECS Service + ALB target + agent-ops sidecar) and returns the new instanceId + public URL. Pass EXACTLY ONE of appType or goal. Use zibby_list_app_templates first to see available appType values AND each entry\'s `architectures` array. Optionally pass provider="codex" to run with the OpenAI Codex agent instead of the default Claude agent — requires the user to have an OpenAI API key staged via zibby_set_openai_credential. Optionally pass architecture="arm64" to run on AWS Graviton (~20% greener at the SAME price — operator pockets the compute savings, user pays the same per-minute rate). Defaults to the catalog\'s first listed architecture (operator-preferred order). If the catalog entry doesn\'t support the requested arch the server returns 400 with a hint listing what it does support.',
605
478
  {
606
- instanceId: z.string().min(1).describe('Instance id (from zibby_list_apps or zibby_deploy_app)'),
479
+ appType: z.string().min(1).optional().describe('Catalog id (e.g. "grafana", "gastown"). MUTUALLY EXCLUSIVE with goal — pass exactly one. Use this when the user wants something from the curated catalog.'),
480
+ goal: z.string().min(1).optional().describe('Free-form description of what to install (e.g. "install n8n on port 5678 with sqlite persistence"). MUTUALLY EXCLUSIVE with appType. Use this when the user wants something NOT in catalog OR has custom config needs — the agent-ops bootstrap will follow the description to install whatever it describes. Examples: "install n8n", "set up an Outline wiki at /wiki", "deploy a basic Rails app from <repo URL>". The user is responsible for any license terms of software they install via this path.'),
481
+ projectId: z.string().min(1).describe('Project the instance attaches to'),
482
+ name: z.string().min(1).optional().describe('Display name for the instance (defaults to appType, or the first line of goal)'),
483
+ provider: z.enum(['claude', 'codex']).optional().describe('Agent provider. Default "claude" (Anthropic). "codex" runs the OpenAI Codex agent and requires an OpenAI API key in workspace-credentials.'),
484
+ 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).'),
607
485
  },
608
- async ({ instanceId }) => {
609
- const res = await callAppsAPI(`/${encodeURIComponent(instanceId)}`);
610
- if (res.error) return { isError: true, content: [{ type: 'text', text: `Network: ${res.error}` }] };
611
- if (!res.ok) return { isError: true, content: [{ type: 'text', text: `Status failed (${res.status})` }] };
612
- return jsonResult(res.body);
486
+ async ({ appType, goal, projectId, name, provider, architecture }) => {
487
+ // Enforce mutual exclusivity client-side so the agent gets a clear
488
+ // error before we burn an HTTP round-trip. Backend enforces the
489
+ // same invariant (apps.js::deployApp) but this gives a faster
490
+ // tighter error loop for the agent.
491
+ if (appType && goal) {
492
+ return { isError: true, content: [{ type: 'text', text: 'Pass either appType (catalog id) OR goal (free-form description), not both.' }] };
493
+ }
494
+ if (!appType && !goal) {
495
+ return { isError: true, content: [{ type: 'text', text: 'Pass either appType (catalog id, see zibby_list_app_templates) OR goal (free-form description) to describe what to deploy.' }] };
496
+ }
497
+ const apiKey = getProjectApiToken(projectId);
498
+ if (!apiKey) {
499
+ return { isError: true, content: [{ type: 'text', text: `No API token cached for project ${projectId}. Run zibby_login to refresh the local project list.` }] };
500
+ }
501
+ // CLI accepts EITHER `app deploy <appType>` OR `app deploy --goal <text>`
502
+ // (see packages/cli/bin/zibby.js); we forward whichever the agent picked.
503
+ const args = ['app', 'deploy'];
504
+ if (appType) args.push(appType);
505
+ if (goal) args.push('--goal', goal);
506
+ args.push('--project', projectId);
507
+ if (name) args.push('--name', name);
508
+ if (provider) args.push('--provider', provider);
509
+ if (architecture) args.push('--arch', architecture);
510
+ return cliResult(await runCli(args, { extraEnv: { ZIBBY_API_KEY: apiKey } }));
613
511
  }
614
512
  );
615
513
 
616
- // ── Tool: app logs ───────────────────────────────────────────────────────
514
+ // ── Tool: status of a single instance — live ECS state, public URL,
515
+ // resources, app version, project.
617
516
  server.tool(
618
- 'zibby_app_logs',
619
- 'Tail the latest N log lines from one hosted app instance. Both the main app container and the agent-ops sidecar log to the same stream; pass `container` to scope.',
517
+ 'zibby_get_app',
518
+ 'Show one deployed app instance: status (running / pending / failed), running task count, app type+version, resources, public URL, project. Use when the user asks "is my app up?" or "what URL is it on?".',
620
519
  {
621
- instanceId: z.string().min(1).describe('Instance id'),
622
- container: z.enum(['app', 'agent-ops']).optional().describe('Which container; defaults to interleaved'),
623
- lines: z.number().int().min(1).max(5000).optional().default(200).describe('How many recent lines'),
520
+ instanceId: z.string().min(1).describe('Instance ID returned from zibby_deploy_app or zibby_list_apps'),
521
+ projectId: z.string().min(1).optional()
522
+ .describe('Project the instance belongs to (lets the tool pick the right cached API token; falls back to session token when omitted)'),
624
523
  },
625
- async ({ instanceId, container, lines }) => {
626
- const qs = new URLSearchParams();
627
- if (container) qs.set('container', container);
628
- if (lines) qs.set('lines', String(lines));
629
- const res = await callAppsAPI(`/${encodeURIComponent(instanceId)}/logs?${qs.toString()}`);
630
- if (res.error) return { isError: true, content: [{ type: 'text', text: `Network: ${res.error}` }] };
631
- if (!res.ok) return { isError: true, content: [{ type: 'text', text: `Logs failed (${res.status})` }] };
632
- return textResult(typeof res.body === 'string' ? res.body : JSON.stringify(res.body, null, 2));
524
+ async ({ instanceId, projectId }) => {
525
+ const extraEnv = {};
526
+ if (projectId) {
527
+ const apiKey = getProjectApiToken(projectId);
528
+ if (apiKey) extraEnv.ZIBBY_API_KEY = apiKey;
529
+ }
530
+ return cliResult(await runCli(['app', 'status', instanceId], { extraEnv }));
633
531
  }
634
532
  );
635
533
 
636
- // ── Daemon MCP proxy ─────────────────────────────────────────────────────
637
- //
638
- // Every tool below routes through POST /apps/{id}/mcp on the Zibby
639
- // backend, which forwards the JSON-RPC body to the per-instance
640
- // agent-ops daemon using the bridgeToken stored in DDB. This means
641
- // users never copy a per-instance token into a local mcp.json — one
642
- // MCP install (@zibby/mcp-cli) covers every instance they own.
643
- //
644
- // Each tool wraps `tools/call` with the daemon's matching agent_*
645
- // builtin (agent_run_now, agent_set_mission, etc.). Helper below
646
- // keeps the JSON-RPC shape and error handling in one place.
647
- async function callDaemonTool(instanceId, daemonToolName, args = {}) {
648
- const body = {
649
- jsonrpc: '2.0',
650
- id: Date.now(),
651
- method: 'tools/call',
652
- params: { name: daemonToolName, arguments: args },
653
- };
654
- const res = await callAppsAPI(`/${encodeURIComponent(instanceId)}/mcp`, {
655
- method: 'POST',
656
- body,
657
- });
658
- if (res.error) return { isError: true, content: [{ type: 'text', text: `Network: ${res.error}` }] };
659
- if (!res.ok) {
660
- const detail = typeof res.body === 'string' ? res.body : JSON.stringify(res.body || {});
661
- return { isError: true, content: [{ type: 'text', text: `Daemon ${daemonToolName} failed (${res.status}): ${detail}` }] };
662
- }
663
- // The daemon returns a JSON-RPC envelope; surface `result` if present,
664
- // otherwise the raw body.
665
- const payload = res.body?.result ?? res.body;
666
- return jsonResult(payload);
667
- }
668
-
534
+ // ── Tool: tail recent logs for a single instance. One-shot snapshot
535
+ // (no streaming over MCP — agents can call again for fresh batches).
669
536
  server.tool(
670
- 'zibby_app_history',
671
- 'Return the recent run history for an instance: each prior task invocation, exit status, and elapsed time. ' +
672
- 'Useful for "did the nightly health-check pass?" type questions.',
537
+ 'zibby_get_app_logs',
538
+ 'Fetch the most recent log lines from a deployed app instance. One-shot snapshot (the user should call again for new lines). Useful for diagnosing why an app is stuck, failed to provision, or to confirm a feature works end-to-end. Returns a string with one event per line, prefixed by ISO timestamp.',
673
539
  {
674
- instanceId: z.string().min(1).describe('Instance id'),
675
- limit: z.number().int().min(1).max(100).optional().default(20),
540
+ instanceId: z.string().min(1).describe('Instance ID returned from zibby_deploy_app or zibby_list_apps'),
541
+ projectId: z.string().min(1).optional().describe('Project the instance belongs to (picks the right cached API token)'),
542
+ lines: z.number().int().min(1).max(5000).optional().default(200)
543
+ .describe('Max number of recent lines to fetch (default 200, max 5000)'),
676
544
  },
677
- async ({ instanceId, limit }) => callDaemonTool(instanceId, 'agent_history', { limit })
545
+ async ({ instanceId, projectId, lines }) => {
546
+ const extraEnv = {};
547
+ if (projectId) {
548
+ const apiKey = getProjectApiToken(projectId);
549
+ if (apiKey) extraEnv.ZIBBY_API_KEY = apiKey;
550
+ }
551
+ const args = ['app', 'logs', instanceId];
552
+ if (lines) args.push('--lines', String(lines));
553
+ return cliResult(await runCli(args, { extraEnv }));
554
+ }
678
555
  );
679
556
 
557
+ // ── Tool: upgrade an instance's agent-ops version in-place.
558
+ // Non-destructive: EFS data, bridgeToken, ALB wiring all preserved.
559
+ // Re-registers the task def with a new image tag, then force-redeploys
560
+ // the ECS Service so the new container picks up the data on the same
561
+ // EFS access point.
680
562
  server.tool(
681
- 'zibby_app_list_tasks',
682
- 'List scheduled tasks configured on an instance (hourly health checks, weekly upgrades, custom cron jobs ' +
683
- 'the user added). Returns name, cron expression, prompt template.',
563
+ 'zibby_upgrade_app',
564
+ 'Upgrade a deployed app instance to a new agent-ops image version IN PLACE. EFS data, ALB wiring, and the per-instance bridgeToken are all preserved — this is NOT a destroy + redeploy. ECS rolls the service: new task pulls the new image, swaps over once healthy. Use after a new agent-ops version is published. Returns the new taskDefinitionArn and the resolved image tag.',
684
565
  {
685
- instanceId: z.string().min(1).describe('Instance id'),
566
+ instanceId: z.string().min(1).describe('Instance ID to upgrade'),
567
+ projectId: z.string().min(1).optional().describe('Project the instance belongs to (picks the right cached API token)'),
568
+ version: z.string().min(1).max(40).optional()
569
+ .describe('Pin to a specific agent-ops version tag (e.g. "0.1.16"). Without this, the upgrade picks up whatever\'s in the AppsFleet base task def in SSM.'),
686
570
  },
687
- async ({ instanceId }) => callDaemonTool(instanceId, 'agent_list_tasks', {})
571
+ async ({ instanceId, projectId, version }) => {
572
+ const extraEnv = {};
573
+ if (projectId) {
574
+ const apiKey = getProjectApiToken(projectId);
575
+ if (apiKey) extraEnv.ZIBBY_API_KEY = apiKey;
576
+ }
577
+ const args = ['app', 'upgrade', instanceId, '--yes'];
578
+ if (version) args.push('--version', version);
579
+ return cliResult(await runCli(args, { extraEnv }));
580
+ }
688
581
  );
689
582
 
583
+ // ── Tool: destroy an instance. DESTRUCTIVE — the agent must confirm
584
+ // with the user before calling. Passes --yes through to skip the CLI's
585
+ // own interactive confirm (which doesn't work over MCP's stdio).
690
586
  server.tool(
691
- 'zibby_app_run_task_now',
692
- 'Trigger an existing scheduled task to run immediately, out-of-cron. The task name comes from ' +
693
- 'zibby_app_list_tasks. The daemon kicks off the LLM run async; poll zibby_app_history to see the result.',
587
+ 'zibby_destroy_app',
588
+ 'Stop and destroy a deployed app instance. DESTRUCTIVE: the running ECS task is stopped, the EFS volume is released, and the instance row is removed. The agent MUST first ask the user for explicit confirmation, then call this with confirm=true.',
694
589
  {
695
- instanceId: z.string().min(1).describe('Instance id'),
696
- taskName: z.string().min(1).describe('Task name from zibby_app_list_tasks'),
590
+ instanceId: z.string().min(1).describe('Instance ID to destroy'),
591
+ projectId: z.string().min(1).optional().describe('Project the instance belongs to (picks the right cached API token)'),
592
+ confirm: z.literal(true).describe('Must be true. Set only after the user has explicitly approved destroying this instance.'),
697
593
  },
698
- async ({ instanceId, taskName }) => callDaemonTool(instanceId, 'agent_run_now', { name: taskName })
594
+ async ({ instanceId, projectId }) => {
595
+ const extraEnv = {};
596
+ if (projectId) {
597
+ const apiKey = getProjectApiToken(projectId);
598
+ if (apiKey) extraEnv.ZIBBY_API_KEY = apiKey;
599
+ }
600
+ return cliResult(await runCli(['app', 'destroy', instanceId, '--yes'], { extraEnv }));
601
+ }
699
602
  );
700
603
 
604
+ // ── Tool: paste / replace a Claude credential (oauth or api).
605
+ // The deploy flow is BYOK ONLY — agents that try to deploy an app for a
606
+ // user with no Claude credential get the 428 CLAUDE_CREDENTIAL_REQUIRED
607
+ // error from zibby_deploy_app, then call this tool to onboard.
608
+ //
609
+ // The token comes from the user; the agent should NOT mint or fabricate
610
+ // it (no oauth flow over MCP). Common path: agent asks user to paste a
611
+ // token, user does, agent calls this with the pasted string.
701
612
  server.tool(
702
- 'zibby_app_get_mission',
703
- 'Read the instance\'s persistent "mission" a free-text instruction the agent-ops daemon ' +
704
- 'carries across every scheduled task run (e.g. "keep n8n on the latest LTS, never auto-major-upgrade").',
613
+ 'zibby_set_claude_credential',
614
+ 'Store a Claude credential (oauth long-lived token from `claude setup-token`, or an Anthropic `sk-ant-…` API key) in the user\'s workspace credentials store. KMS-encrypted server-side; the plaintext is sent over HTTPS but never persisted client-side. Auto-detects oauth vs api by token prefix (`sk-ant-oat` / `oat_` → oauth; `sk-ant-api` → api). Use this AFTER the user pastes a token, NOT to mint one yourself. After this succeeds, retry the original zibby_deploy_app call.',
705
615
  {
706
- instanceId: z.string().min(1).describe('Instance id'),
616
+ token: z.string().min(8).describe('The Claude token the user pasted. Either an OAuth subscription token from `claude setup-token` (long-lived, bills against Claude Code sub) or an Anthropic API key starting with sk-ant-… (bills against Anthropic API credits).'),
617
+ type: z.enum(['oauth', 'api']).optional().describe('Force the credential kind. Omit to auto-detect from token prefix.'),
707
618
  },
708
- async ({ instanceId }) => callDaemonTool(instanceId, 'agent_get_mission', {})
619
+ async ({ token, type }) => {
620
+ const args = ['creds', 'set', 'claude', token];
621
+ if (type) args.push('--type', type);
622
+ return cliResult(await runCli(args));
623
+ }
709
624
  );
710
625
 
626
+ // ── Tool: paste an OpenAI API key for Codex deploys. Mirror of the
627
+ // Claude tool above. Codex v1 is API-key only (no OAuth flow), so the
628
+ // kind is hardcoded api server-side; agents only need to pass the
629
+ // token. The deploy flow is BYOK — agents that try a Codex deploy
630
+ // without an OpenAI cred get a 428 OPENAI_CREDENTIAL_REQUIRED error
631
+ // from zibby_deploy_app, then call this tool to onboard.
711
632
  server.tool(
712
- 'zibby_app_set_mission',
713
- 'Set the instance\'s persistent mission. Replaces the prior value. Confirm with the user before calling ' +
714
- 'changing the mission alters the daemon\'s autonomous behavior on every subsequent task.',
633
+ 'zibby_set_openai_credential',
634
+ 'Store an OpenAI API key in the user\'s workspace credentials store, for use by the Codex agent on Managed App deploys. KMS-encrypted server-side; the plaintext is sent over HTTPS but never persisted client-side. API-key only (Codex v1 has no OAuth flow). Use this AFTER the user pastes their key, NOT to mint one yourself. After this succeeds, retry the original zibby_deploy_app call with provider="codex".',
715
635
  {
716
- instanceId: z.string().min(1).describe('Instance id'),
717
- mission: z.string().min(1).max(4000).describe('Free-text instruction the daemon carries forward'),
636
+ token: z.string().min(8).describe('The OpenAI API key the user pasted (typically starts with sk-…). Mint one at https://platform.openai.com/api-keys. Bills against the user\'s OpenAI account.'),
718
637
  },
719
- async ({ instanceId, mission }) => callDaemonTool(instanceId, 'agent_set_mission', { mission })
638
+ async ({ token }) => {
639
+ const args = ['creds', 'set', 'openai', token];
640
+ return cliResult(await runCli(args));
641
+ }
720
642
  );
721
643
 
722
- // ── Tool: destroy app (guarded) ──────────────────────────────────────────
644
+ // ── Tool: rotate the per-instance Claude credential to whatever's
645
+ // currently in the user's workspace-credentials. EFS data + bridgeToken
646
+ // + ALB wiring all preserved; ECS Service rolls the task ~30s.
647
+ //
648
+ // Use after `zibby_set_claude_credential` rotated the workspace cred,
649
+ // to push the change to already-running app instances.
723
650
  server.tool(
724
- 'zibby_destroy_app',
725
- 'PERMANENTLY destroy a hosted app instance: stops the Fargate task, detaches EFS, revokes the agent-ops MCP token. ' +
726
- 'DESTRUCTIVE — the agent MUST confirm with the user and pass confirm:true.',
651
+ 'zibby_update_app_credential',
652
+ 'Rotate the per-instance Claude credential on a running app instance to whatever is currently stored in the user\'s workspace-credentials. Use after the user has uploaded a new token (via zibby_set_claude_credential) and wants existing instances to pick it up. EFS data, ALB wiring, and the bridgeToken are preserved. The task restarts (~30s) and ECS swaps over.',
727
653
  {
728
- instanceId: z.string().min(1).describe('Instance id to destroy'),
729
- confirm: z.literal(true).describe('Must be true. Set only after user has explicitly authorized destroying this instance.'),
730
- keepData: z.boolean().optional().default(false).describe('If true, snapshots the EFS before detach so a future redeploy can restore.'),
654
+ instanceId: z.string().min(1).describe('Instance ID to rotate the credential on'),
655
+ projectId: z.string().min(1).optional().describe('Project the instance belongs to (picks the right cached API token)'),
731
656
  },
732
- async ({ instanceId, keepData }) => {
733
- const res = await callAppsAPI(`/${encodeURIComponent(instanceId)}`, {
734
- method: 'DELETE',
735
- body: { keepData },
736
- });
737
- if (res.error) return { isError: true, content: [{ type: 'text', text: `Network: ${res.error}` }] };
738
- if (!res.ok) return { isError: true, content: [{ type: 'text', text: `Destroy failed (${res.status})` }] };
739
- return jsonResult(res.body);
657
+ async ({ instanceId, projectId }) => {
658
+ const extraEnv = {};
659
+ if (projectId) {
660
+ const apiKey = getProjectApiToken(projectId);
661
+ if (apiKey) extraEnv.ZIBBY_API_KEY = apiKey;
662
+ }
663
+ return cliResult(await runCli(['app', 'update-credential', instanceId], { extraEnv }));
740
664
  }
741
665
  );
742
666
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zibby/mcp-cli",
3
- "version": "0.2.1",
4
- "description": "Zibby CLI MCP Server — expose Zibby workflow deploy/run/debug to AI agents (Claude, Cursor, Codex, Gemini)",
3
+ "version": "0.3.2",
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",
7
7
  "bin": {