@zibby/mcp-cli 0.1.0
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/LICENSE +21 -0
- package/README.md +180 -0
- package/index.js +505 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zibby
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# @zibby/mcp-cli
|
|
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.
|
|
4
|
+
|
|
5
|
+
## What you get
|
|
6
|
+
|
|
7
|
+
13 MCP tools wrapping the same things `@zibby/cli` does:
|
|
8
|
+
|
|
9
|
+
| Tool | What it does |
|
|
10
|
+
|---|---|
|
|
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.
|
|
26
|
+
|
|
27
|
+
## Prerequisites
|
|
28
|
+
|
|
29
|
+
- **Node.js ≥ 18** on the user's machine
|
|
30
|
+
- A Zibby account (sign up at [zibby.dev](https://zibby.dev))
|
|
31
|
+
|
|
32
|
+
That's it. No global `npm install` needed — `npx` handles the bundle (which includes `@zibby/cli`).
|
|
33
|
+
|
|
34
|
+
## Install (per agent)
|
|
35
|
+
|
|
36
|
+
### Claude Code
|
|
37
|
+
|
|
38
|
+
`~/.claude/settings.json` (or `~/.claude.json` on older versions):
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"zibby": {
|
|
44
|
+
"command": "npx",
|
|
45
|
+
"args": ["-y", "@zibby/mcp-cli"]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then in Claude Code: "log in to Zibby" → it'll call `zibby_login` and open the browser.
|
|
52
|
+
|
|
53
|
+
### Cursor
|
|
54
|
+
|
|
55
|
+
`~/.cursor/mcp.json`:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"mcpServers": {
|
|
60
|
+
"zibby": {
|
|
61
|
+
"command": "npx",
|
|
62
|
+
"args": ["-y", "@zibby/mcp-cli"]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### OpenAI Codex CLI
|
|
69
|
+
|
|
70
|
+
`~/.codex/config.toml`:
|
|
71
|
+
|
|
72
|
+
```toml
|
|
73
|
+
[mcp_servers.zibby]
|
|
74
|
+
command = "npx"
|
|
75
|
+
args = ["-y", "@zibby/mcp-cli"]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Gemini CLI
|
|
79
|
+
|
|
80
|
+
`~/.gemini/settings.json`:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"zibby": {
|
|
86
|
+
"command": "npx",
|
|
87
|
+
"args": ["-y", "@zibby/mcp-cli"]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Claude Desktop (macOS)
|
|
94
|
+
|
|
95
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"mcpServers": {
|
|
100
|
+
"zibby": {
|
|
101
|
+
"command": "npx",
|
|
102
|
+
"args": ["-y", "@zibby/mcp-cli"]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Windows note
|
|
109
|
+
|
|
110
|
+
If your agent on Windows can't find `npx`, wrap it in `cmd /c`:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"mcpServers": {
|
|
115
|
+
"zibby": {
|
|
116
|
+
"command": "cmd",
|
|
117
|
+
"args": ["/c", "npx", "-y", "@zibby/mcp-cli"]
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Authentication
|
|
124
|
+
|
|
125
|
+
Two-stage by design (mirrors how the `zibby` CLI works):
|
|
126
|
+
|
|
127
|
+
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`.
|
|
129
|
+
|
|
130
|
+
All credentials live in `~/.zibby/config.json` (mode `0600`). The MCP server reads/writes that file directly — no separate credential store.
|
|
131
|
+
|
|
132
|
+
The user's password never touches the MCP server: login is OAuth in the browser, and only the resulting session token comes back to the local file.
|
|
133
|
+
|
|
134
|
+
## How a typical agent chat flow looks
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
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
|
+
→ "Deployed v1 of playhouse-tests. UUID 988…"
|
|
143
|
+
|
|
144
|
+
User: Run it against staging.zibby.dev.
|
|
145
|
+
Agent: → zibby_trigger_workflow (input: { url: "https://staging.zibby.dev" })
|
|
146
|
+
→ returns { jobId: "abc-123" }
|
|
147
|
+
→ zibby_workflow_logs (lines: 200, jobId: "abc-123")
|
|
148
|
+
→ "Run completed. Found 0 errors."
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Troubleshooting
|
|
152
|
+
|
|
153
|
+
| Problem | Likely cause |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `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. |
|
|
157
|
+
| 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
|
+
| `npx -y` hangs on first install | First-time download. Subsequent invocations are cached. |
|
|
159
|
+
| 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. |
|
|
160
|
+
|
|
161
|
+
## Security model
|
|
162
|
+
|
|
163
|
+
- **No shell interpolation** — all CLI invocations use `execFile` with argv arrays
|
|
164
|
+
- **Minimum env passthrough** — only `HOME`, `USER`, `PATH`, and the project-scoped `ZIBBY_API_KEY` are forwarded to the child CLI process
|
|
165
|
+
- **Destructive ops excluded** — see "What you get" above
|
|
166
|
+
- **`zibby_download_workflow` requires `confirm: true`** — agent must explicitly opt in after confirming dest path with the user
|
|
167
|
+
- **API tokens never returned to the agent** — they live in `~/.zibby/config.json` only
|
|
168
|
+
|
|
169
|
+
## Versioning
|
|
170
|
+
|
|
171
|
+
`@zibby/mcp-cli` pins a specific `@zibby/cli` version in its `dependencies`. Upgrading the MCP package upgrades the bundled CLI in lockstep. To check which CLI version is bundled:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
npx -y @zibby/mcp-cli --version # MCP server version
|
|
175
|
+
node -p "require('@zibby/cli/package.json').version" # bundled CLI version (after first npx run)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* global process */
|
|
3
|
+
/**
|
|
4
|
+
* Zibby CLI MCP Server
|
|
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.
|
|
9
|
+
*
|
|
10
|
+
* Distribution model: stdio MCP server published to npm. Spawned by the
|
|
11
|
+
* 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).
|
|
15
|
+
*
|
|
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).
|
|
19
|
+
*
|
|
20
|
+
* Auth: login is implemented in-process via the device-code OAuth flow
|
|
21
|
+
* against api-prod.zibby.app/cli/login/{initiate,poll}. We mirror the file
|
|
22
|
+
* format that @zibby/cli's saveSessionToken / saveProjects write, so the
|
|
23
|
+
* shelled-out CLI commands pick up credentials automatically.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
27
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
import { execFile, spawn } from 'node:child_process';
|
|
30
|
+
import { promisify } from 'node:util';
|
|
31
|
+
import { createRequire } from 'node:module';
|
|
32
|
+
import { homedir } from 'node:os';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
35
|
+
|
|
36
|
+
const exec = promisify(execFile);
|
|
37
|
+
const require = createRequire(import.meta.url);
|
|
38
|
+
|
|
39
|
+
// ── Constants ───────────────────────────────────────────────────────────
|
|
40
|
+
|
|
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');
|
|
45
|
+
|
|
46
|
+
// Mirrors @zibby/cli's config storage. We read/write the same file so the
|
|
47
|
+
// CLI commands we shell out to pick up our login state, and vice versa.
|
|
48
|
+
const CONFIG_DIR = join(homedir(), '.zibby');
|
|
49
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
50
|
+
|
|
51
|
+
const API_BASE = process.env.ZIBBY_API_URL || 'https://api-prod.zibby.app';
|
|
52
|
+
|
|
53
|
+
// ── Config file helpers (matches @zibby/cli/src/config/config.js shape) ─
|
|
54
|
+
|
|
55
|
+
function loadConfig() {
|
|
56
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')) || {};
|
|
59
|
+
} catch {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function saveConfig(cfg) {
|
|
65
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
66
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getSessionToken() { return loadConfig().sessionToken || null; }
|
|
70
|
+
function getUserInfo() { return loadConfig().user || null; }
|
|
71
|
+
function getProjects() { return loadConfig().projects || []; }
|
|
72
|
+
|
|
73
|
+
function clearSession() {
|
|
74
|
+
const cfg = loadConfig();
|
|
75
|
+
delete cfg.sessionToken;
|
|
76
|
+
delete cfg.user;
|
|
77
|
+
delete cfg.projects;
|
|
78
|
+
delete cfg.proxyUrl;
|
|
79
|
+
delete cfg.mem0ProxyUrl;
|
|
80
|
+
saveConfig(cfg);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Look up a project's per-project API token (project-scoped, what workflow
|
|
85
|
+
* 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).
|
|
87
|
+
*/
|
|
88
|
+
function getProjectApiToken(projectId) {
|
|
89
|
+
return getProjects().find((p) => p.projectId === projectId)?.apiToken || null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Shell helpers ───────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Run the bundled @zibby/cli with the given argv. Uses execFile so there's
|
|
96
|
+
* no shell interpretation — every value in args is a literal argv entry.
|
|
97
|
+
* Returns { code, stdout, stderr }. Never throws (we want to surface the
|
|
98
|
+
* exit code + stderr to the agent rather than crashing the MCP server).
|
|
99
|
+
*/
|
|
100
|
+
async function runCli(args, opts = {}) {
|
|
101
|
+
try {
|
|
102
|
+
const { stdout, stderr } = await exec(process.execPath, [ZIBBY_BIN, ...args], {
|
|
103
|
+
// Inherit HOME so the CLI finds ~/.zibby/config.json, and PATH so
|
|
104
|
+
// sub-tools (npm, node, dolt) are reachable. Anything else stays
|
|
105
|
+
// out — we don't want stray env vars leaking into the deploy.
|
|
106
|
+
env: {
|
|
107
|
+
HOME: process.env.HOME,
|
|
108
|
+
USER: process.env.USER,
|
|
109
|
+
PATH: process.env.PATH,
|
|
110
|
+
TERM: 'dumb', // suppress ora spinners
|
|
111
|
+
NO_COLOR: '1', // strip chalk colors
|
|
112
|
+
DOTENV_CONFIG_QUIET: 'true',
|
|
113
|
+
...(opts.extraEnv || {}),
|
|
114
|
+
},
|
|
115
|
+
cwd: opts.cwd || process.cwd(),
|
|
116
|
+
timeout: opts.timeout || 600_000, // 10 min default
|
|
117
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
118
|
+
});
|
|
119
|
+
return { code: 0, stdout, stderr };
|
|
120
|
+
} catch (err) {
|
|
121
|
+
// execFile rejects with { code, stdout, stderr } on non-zero exit.
|
|
122
|
+
return {
|
|
123
|
+
code: typeof err.code === 'number' ? err.code : 1,
|
|
124
|
+
stdout: err.stdout || '',
|
|
125
|
+
stderr: err.stderr || String(err.message || err),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Format a runCli result as an MCP tool response. */
|
|
131
|
+
function cliResult(r) {
|
|
132
|
+
const body =
|
|
133
|
+
r.code === 0
|
|
134
|
+
? r.stdout || '(command produced no output)'
|
|
135
|
+
: `Command failed (exit ${r.code})\n\nSTDOUT:\n${r.stdout || '(empty)'}\n\nSTDERR:\n${r.stderr || '(empty)'}`;
|
|
136
|
+
return {
|
|
137
|
+
isError: r.code !== 0,
|
|
138
|
+
content: [{ type: 'text', text: body }],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Convert a plain JSON object into a text MCP response. */
|
|
143
|
+
function jsonResult(obj) {
|
|
144
|
+
return { content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }] };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Convert a string into a text MCP response. */
|
|
148
|
+
function textResult(text) {
|
|
149
|
+
return { content: [{ type: 'text', text }] };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Cross-platform "open URL in the user's default browser". Detached so
|
|
154
|
+
* the MCP server doesn't block waiting for the browser process. Safe
|
|
155
|
+
* (no shell interpolation).
|
|
156
|
+
*/
|
|
157
|
+
function openBrowser(url) {
|
|
158
|
+
try {
|
|
159
|
+
const platform = process.platform;
|
|
160
|
+
let cmd; let args;
|
|
161
|
+
if (platform === 'darwin') { cmd = 'open'; args = [url]; }
|
|
162
|
+
else if (platform === 'win32') { cmd = 'cmd'; args = ['/c', 'start', '', url]; }
|
|
163
|
+
else { cmd = 'xdg-open'; args = [url]; }
|
|
164
|
+
spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
|
|
165
|
+
return true;
|
|
166
|
+
} catch { return false; }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
170
|
+
|
|
171
|
+
// ── MCP server ─────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
const server = new McpServer({
|
|
174
|
+
name: 'zibby-cli',
|
|
175
|
+
version: '0.1.0',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── Tool: login ─────────────────────────────────────────────────────────
|
|
179
|
+
// Device-code OAuth flow. Opens the user's browser to the verification URL,
|
|
180
|
+
// then polls /cli/login/poll until the user authorizes (or it times out).
|
|
181
|
+
// Writes session + projects to ~/.zibby/config.json on success.
|
|
182
|
+
server.tool(
|
|
183
|
+
'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.',
|
|
185
|
+
{},
|
|
186
|
+
async () => {
|
|
187
|
+
// If already logged in, short-circuit — agent shouldn't trigger a
|
|
188
|
+
// fresh login dance every time the session is good.
|
|
189
|
+
const existing = getSessionToken();
|
|
190
|
+
if (existing) {
|
|
191
|
+
const user = getUserInfo();
|
|
192
|
+
return textResult(`Already logged in as ${user?.email || 'unknown'}. Call zibby_logout first if you want to switch accounts.`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 1. Request device code.
|
|
196
|
+
const initRes = await fetch(`${API_BASE}/cli/login/initiate`, {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: { 'Content-Type': 'application/json' },
|
|
199
|
+
});
|
|
200
|
+
if (!initRes.ok) {
|
|
201
|
+
const errText = await initRes.text().catch(() => '');
|
|
202
|
+
return { isError: true, content: [{ type: 'text', text: `Failed to initiate login: ${initRes.status} ${errText}` }] };
|
|
203
|
+
}
|
|
204
|
+
const init = await initRes.json();
|
|
205
|
+
const { deviceCode, verificationUrl, expiresIn, interval } = init;
|
|
206
|
+
|
|
207
|
+
// 2. Open browser. Don't fail if the OS can't open one — agent surfaces
|
|
208
|
+
// the URL so the user can paste it manually.
|
|
209
|
+
const opened = openBrowser(verificationUrl);
|
|
210
|
+
|
|
211
|
+
// 3. Poll until authorized / denied / timeout. expiresIn is in seconds.
|
|
212
|
+
const pollIntervalMs = (interval || 3) * 1000;
|
|
213
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
214
|
+
while (Date.now() < deadline) {
|
|
215
|
+
await sleep(pollIntervalMs);
|
|
216
|
+
const pollRes = await fetch(`${API_BASE}/cli/login/poll`, {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers: { 'Content-Type': 'application/json' },
|
|
219
|
+
body: JSON.stringify({ deviceCode }),
|
|
220
|
+
});
|
|
221
|
+
if (pollRes.status === 202) continue; // still waiting
|
|
222
|
+
if (!pollRes.ok) {
|
|
223
|
+
const errText = await pollRes.text().catch(() => '');
|
|
224
|
+
return { isError: true, content: [{ type: 'text', text: `Login poll failed: ${pollRes.status} ${errText}` }] };
|
|
225
|
+
}
|
|
226
|
+
const result = await pollRes.json();
|
|
227
|
+
if (result.status === 'denied') {
|
|
228
|
+
return { isError: true, content: [{ type: 'text', text: 'User denied the login authorization in the browser.' }] };
|
|
229
|
+
}
|
|
230
|
+
if (result.status === 'authorized') {
|
|
231
|
+
// Fetch projects to seed the saved list (workflow ops need per-
|
|
232
|
+
// project apiTokens). Best-effort — if it fails we still save the
|
|
233
|
+
// session token and the user can rerun zibby_list_projects.
|
|
234
|
+
let projects = [];
|
|
235
|
+
try {
|
|
236
|
+
const pRes = await fetch(`${API_BASE}/projects`, {
|
|
237
|
+
headers: { Authorization: `Bearer ${result.token}` },
|
|
238
|
+
});
|
|
239
|
+
if (pRes.ok) {
|
|
240
|
+
const data = await pRes.json();
|
|
241
|
+
projects = (data.projects || []).map((p) => ({
|
|
242
|
+
name: p.name,
|
|
243
|
+
projectId: p.projectId,
|
|
244
|
+
apiToken: p.apiToken,
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
} catch { /* non-fatal */ }
|
|
248
|
+
saveConfig({
|
|
249
|
+
...loadConfig(),
|
|
250
|
+
sessionToken: result.token,
|
|
251
|
+
user: result.user,
|
|
252
|
+
proxyUrl: result.proxyUrl,
|
|
253
|
+
mem0ProxyUrl: result.mem0ProxyUrl,
|
|
254
|
+
projects,
|
|
255
|
+
});
|
|
256
|
+
return textResult(
|
|
257
|
+
`Logged in as ${result.user?.email || 'unknown'} (${projects.length} project${projects.length !== 1 ? 's' : ''} cached).${opened ? '' : `\n\nBrowser could not open automatically. Verification URL was: ${verificationUrl}`}`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { isError: true, content: [{ type: 'text', text: 'Login timed out before the user completed authorization. Call zibby_login again to retry.' }] };
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// ── Tool: logout ────────────────────────────────────────────────────────
|
|
266
|
+
server.tool(
|
|
267
|
+
'zibby_logout',
|
|
268
|
+
'Clear the saved session and project tokens from ~/.zibby/config.json. After logout, zibby_login is required before any other tool call.',
|
|
269
|
+
{},
|
|
270
|
+
async () => {
|
|
271
|
+
if (!getSessionToken()) return textResult('Not logged in.');
|
|
272
|
+
const email = getUserInfo()?.email;
|
|
273
|
+
clearSession();
|
|
274
|
+
return textResult(`Logged out${email ? ` (${email})` : ''}.`);
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// ── Tool: status ────────────────────────────────────────────────────────
|
|
279
|
+
server.tool(
|
|
280
|
+
'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.',
|
|
282
|
+
{},
|
|
283
|
+
async () => {
|
|
284
|
+
const token = getSessionToken();
|
|
285
|
+
if (!token) return textResult('Not logged in. Call zibby_login.');
|
|
286
|
+
const user = getUserInfo();
|
|
287
|
+
const projects = getProjects();
|
|
288
|
+
// Validate token by hitting a cheap authed endpoint.
|
|
289
|
+
let apiOk = null;
|
|
290
|
+
try {
|
|
291
|
+
const res = await fetch(`${API_BASE}/projects`, {
|
|
292
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
293
|
+
});
|
|
294
|
+
apiOk = res.ok;
|
|
295
|
+
} catch { apiOk = false; }
|
|
296
|
+
return jsonResult({
|
|
297
|
+
loggedIn: true,
|
|
298
|
+
user: { email: user?.email, name: user?.name },
|
|
299
|
+
cachedProjects: projects.length,
|
|
300
|
+
tokenValid: apiOk,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
|
|
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
|
+
// ── Tool: list templates ────────────────────────────────────────────────
|
|
332
|
+
server.tool(
|
|
333
|
+
'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.',
|
|
335
|
+
{},
|
|
336
|
+
async () => cliResult(await runCli(['template', 'list']))
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// ── Tool: scaffold workflow ─────────────────────────────────────────────
|
|
340
|
+
server.tool(
|
|
341
|
+
'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.',
|
|
343
|
+
{
|
|
344
|
+
name: z.string().min(1).describe('Local workflow folder name (kebab-case)'),
|
|
345
|
+
template: z.enum(['browser-test-automation', 'code-analysis', 'generate-test-cases'])
|
|
346
|
+
.describe('Official template to scaffold from'),
|
|
347
|
+
skipInstall: z.boolean().optional().default(false)
|
|
348
|
+
.describe('Skip running `npm install` in the new workflow folder'),
|
|
349
|
+
},
|
|
350
|
+
async ({ name, template, skipInstall }) => {
|
|
351
|
+
const args = ['g', 'workflow', name, '-t', template, '--no-agent-helpers'];
|
|
352
|
+
if (skipInstall) args.push('--skip-install');
|
|
353
|
+
return cliResult(await runCli(args));
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// ── Tool: validate workflow ─────────────────────────────────────────────
|
|
358
|
+
server.tool(
|
|
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.',
|
|
361
|
+
{
|
|
362
|
+
name: z.string().min(1).describe('Workflow folder name under .zibby/workflows/'),
|
|
363
|
+
},
|
|
364
|
+
async ({ name }) => cliResult(await runCli(['workflow', 'validate', name]))
|
|
365
|
+
);
|
|
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
|
+
// ── Tool: deploy workflow ───────────────────────────────────────────────
|
|
389
|
+
server.tool(
|
|
390
|
+
'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.',
|
|
392
|
+
{
|
|
393
|
+
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)'),
|
|
395
|
+
force: z.boolean().optional().default(false)
|
|
396
|
+
.describe('Re-deploy even if source checksum is unchanged'),
|
|
397
|
+
warm: z.number().int().min(1).max(5).optional()
|
|
398
|
+
.describe('Enable warm-pool execution (1-5 always-on Fargate tasks) — paid feature, skips ~60s cold start'),
|
|
399
|
+
},
|
|
400
|
+
async ({ name, projectId, force, warm }) => {
|
|
401
|
+
const apiKey = getProjectApiToken(projectId);
|
|
402
|
+
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.` }] };
|
|
404
|
+
}
|
|
405
|
+
const args = ['workflow', 'deploy', name, '--project', projectId];
|
|
406
|
+
if (force) args.push('--force');
|
|
407
|
+
if (warm) args.push('--warm', String(warm));
|
|
408
|
+
return cliResult(await runCli(args, { extraEnv: { ZIBBY_API_KEY: apiKey } }));
|
|
409
|
+
}
|
|
410
|
+
);
|
|
411
|
+
|
|
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
|
+
// ── Tool: run workflow locally ──────────────────────────────────────────
|
|
463
|
+
server.tool(
|
|
464
|
+
'zibby_run_workflow_local',
|
|
465
|
+
'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
|
+
{
|
|
467
|
+
name: z.string().min(1).describe('Local workflow folder name'),
|
|
468
|
+
input: z.record(z.string(), z.any()).optional().default({})
|
|
469
|
+
.describe('Input params (same shape as zibby_trigger_workflow)'),
|
|
470
|
+
},
|
|
471
|
+
async ({ name, input }) => {
|
|
472
|
+
const args = ['workflow', 'run', name, '--input', JSON.stringify(input || {})];
|
|
473
|
+
return cliResult(await runCli(args, { timeout: 30 * 60 * 1000 })); // local runs can be longer
|
|
474
|
+
}
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
// ── Tool: download workflow ─────────────────────────────────────────────
|
|
478
|
+
// Destructive enough to require explicit user confirmation since it can
|
|
479
|
+
// overwrite files in the user's working directory. The agent MUST set
|
|
480
|
+
// `confirm: true` AND provide a `dest` path it has shown to the user.
|
|
481
|
+
server.tool(
|
|
482
|
+
'zibby_download_workflow',
|
|
483
|
+
'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.',
|
|
484
|
+
{
|
|
485
|
+
uuid: z.string().min(1).describe('Workflow UUID to download'),
|
|
486
|
+
projectId: z.string().min(1).describe('Project the workflow lives under'),
|
|
487
|
+
dest: z.string().min(1).describe('Destination directory path (absolute or relative to cwd). Show this to the user before calling.'),
|
|
488
|
+
confirm: z.literal(true).describe('Must be true. Set only after the user has explicitly approved the download to the specified dest.'),
|
|
489
|
+
force: z.boolean().optional().default(false)
|
|
490
|
+
.describe('Overwrite without prompting + bypass uuid-mismatch guard (set only after the user explicitly authorizes overwriting existing files)'),
|
|
491
|
+
},
|
|
492
|
+
async ({ uuid, projectId, dest, force }) => {
|
|
493
|
+
const apiKey = getProjectApiToken(projectId);
|
|
494
|
+
if (!apiKey) {
|
|
495
|
+
return { isError: true, content: [{ type: 'text', text: `No API token cached for project ${projectId}. Run zibby_list_projects.` }] };
|
|
496
|
+
}
|
|
497
|
+
const args = ['workflow', 'download', uuid, '--dest', dest];
|
|
498
|
+
if (force) args.push('--force');
|
|
499
|
+
return cliResult(await runCli(args, { extraEnv: { ZIBBY_API_KEY: apiKey } }));
|
|
500
|
+
}
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
// ── Connect ─────────────────────────────────────────────────────────────
|
|
504
|
+
|
|
505
|
+
await server.connect(new StdioServerTransport());
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zibby/mcp-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zibby CLI MCP Server — expose Zibby workflow deploy/run/debug to AI agents (Claude, Cursor, Codex, Gemini)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-cli-zibby": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node index.js",
|
|
17
|
+
"lint": "eslint .",
|
|
18
|
+
"lint:fix": "eslint --fix ."
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"zibby",
|
|
23
|
+
"workflow",
|
|
24
|
+
"agent",
|
|
25
|
+
"deploy",
|
|
26
|
+
"playwright"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/ZibbyHQ/zibby-agent"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
34
|
+
"@zibby/cli": "^0.4.30",
|
|
35
|
+
"zod": "^4.3.6"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"author": "Zibby",
|
|
42
|
+
"homepage": "https://zibby.dev"
|
|
43
|
+
}
|