# vantuly CLI and MCP server

Deploy a folder to one of your team's servers from a terminal or an AI agent (Claude Code, Codex,
OpenCode, Cursor). The site goes live on a free `https://NAME.vantuly.site` address at once; add
your own domain whenever you like. Your server, your domain.

You need a Vantuly account with at least one ready server. Add servers in the panel at
https://app.vantuly.com.

## 1. Install

With Node.js 18 or newer (recommended, any OS):

```sh
npx -y vantuly <command>
```

macOS and Linux without Node.js:

```sh
curl -fsSL https://app.vantuly.com/cli.sh | sh
```

This puts the binary in `~/.local/bin/vantuly`. `VANTULY_INSTALL_DIR` picks another folder and
`VANTULY_VERSION=v1.2.3` a version. No root is needed. If the script says so, add the folder to
your `PATH`. A self-hosted panel serves its own script at `<panel>/cli.sh`.

With Python's [uv](https://docs.astral.sh/uv/), or pip (any OS):

```sh
uvx vantuly <command>        # or: pipx install vantuly, pip install vantuly
```

With Homebrew (macOS, Linux):

```sh
brew install vantuly/tap/vantuly
```

Windows: use `npx -y vantuly` or `uvx vantuly`, or download `vantuly-windows-amd64.exe` (or `-arm64.exe`) from
`https://cli.vantuly.com/<version>/`. The newest version number is in
`https://cli.vantuly.com/latest`.

npx, `cli.sh` and Homebrew download from `https://cli.vantuly.com/<version>/` and refuse a binary
whose SHA-256 does not match the release's `checksums.txt`. The PyPI package has one wheel per
platform with the binary inside.

Check the version:

```sh
vantuly --version
```

Updating: `npx -y vantuly` runs the newest published version (each version is downloaded once and
cached in `~/.cache/vantuly`), and so does `uvx vantuly@latest`. With `cli.sh`, run the install
line again; with Homebrew, `brew upgrade vantuly`.

The examples below write `vantuly`; with npx, read it as `npx -y vantuly`, with uv as `uvx vantuly`.

## 2. Sign in

```sh
vantuly login
```

The CLI prints a code and a link and opens your browser. In the browser you confirm your
password, pick the team, check that the code matches the one in the terminal, and approve. The
terminal then prints who you are signed in as. The code is valid once, for 10 minutes. Your email
address must be verified before you can approve.

You get a **deploy token**. It is bound to that one team, expires after 90 days, and can only list
servers and sites, create sites, deploy, roll back, add domains and set environment variables. It
cannot delete anything, open a terminal, change the firewall, read environment values back, or
touch members, billing or other tokens.

```sh
vantuly login --full                            # a full-access token (every team) instead
vantuly login --url https://panel.example.com   # a self-hosted panel
vantuly whoami                                  # user, team, token scope and expiry
vantuly logout                                  # revokes the token and forgets it
```

For CI and sandboxes, set `VANTULY_TOKEN` (and `VANTULY_URL` for a self-hosted panel) instead of
running `login`. They win over the saved sign-in. `logout` never revokes a token that came from
`VANTULY_TOKEN`.

## 3. Your first deploy

See your servers:

```sh
vantuly servers
```

Build the site and deploy its output folder:

```sh
npm run build
vantuly deploy dist
```

```text
Creating a static site on web-1 ...
Packing dist ...
Uploading 42 files (1.3 MB) ...
Deploying ...
https://my-blog.vantuly.site
```

The first deploy creates a site:

- On the team's only ready server. With more than one, pass `--server NAME` (a name or ID).
- On a free `https://NAME.vantuly.site` address. NAME comes from the folder; for `dist`, `build`,
  `out`, `public`, `_site` and `site` it is the project folder's name. `--name shop` gives
  `shop.vantuly.site`. If the name is taken, the panel picks a free one: the URL printed at the
  end is the address.
- `--domain example.com` puts the site on your own domain instead of a free address.

It also writes `.vantuly/project.json` in the folder you run the command from (with a
`.gitignore`, so it is not committed). Later deploys from that folder update the same site:

```sh
vantuly deploy dist
```

`--site ID` deploys to any site of the team. `--no-wait` returns once the upload is done.

## 4. What gets deployed

- **A built static site**: a folder with `index.html` at the top. Astro, Vite, SvelteKit
  (`adapter-static`), Gatsby, Eleventy and VitePress projects must be built first. The CLI never
  runs your build; it tells you which folder to pass: `dist` (Astro, Vite), `build` (SvelteKit),
  `public` (Gatsby), `_site` (Eleventy), `.vitepress/dist` (VitePress). Next.js is not supported
  yet, except a static export (`output: "export"`, then `vantuly deploy out`).
- **Node.js and Python apps** deploy as source and are built on the server: a `package.json`
  project runs `npm ci && npm run build`; a `requirements.txt` or `pyproject.toml` project is
  installed with uv.
- The type is detected. `--type static|node|astro|python` sets it.

Left out of the upload: `.git`, `node_modules`, `.vantuly`, `.env` files (`.env.example` and the
like stay) and Finder files. Add more in `.vantulyignore`, one pattern per line, `#` for comments:

```text
# a name matches at any depth
*.log
# a pattern with / matches from the top
docs/drafts
# a trailing / matches folders only
tmp/
```

There is no `!` negation. Links and special files are refused. The zip may be at most 200 MB.

Each deploy is a new release. The switch is atomic, and when a build fails the live site keeps
running.

## 5. Everyday commands

Commands that act on a site use `--site ID`, else the folder's linked site.

```sh
vantuly deploy dist              # deploy again
vantuly deployments              # history: ID, status, trigger, time
vantuly logs                     # the latest build log (follows a running deploy)
vantuly logs DEPLOYMENT_ID       # a given deployment's log
vantuly rollback                 # put the release before the live one back
vantuly rollback DEPLOYMENT_ID   # put that deployment's release back
```

Environment variables. You only ever see names; values are write-only. Deploy again to use them:

```sh
vantuly env                                       # names
vantuly env set API_URL=https://api.example.com KEY=secret
vantuly env unset KEY
vantuly deploy dist
```

Domains:

```sh
vantuly domains                  # each domain and whether DNS points at the server
vantuly domains add example.com
```

If the team connected Cloudflare, the DNS records are created for you. Otherwise the CLI prints
the A (and AAAA) record to create. HTTPS is issued once DNS points at the server; run
`vantuly domains` to check.

Other commands:

```sh
vantuly link --site ID          # link this folder to an existing site
vantuly open                    # open the site in the browser
vantuly open --panel            # open its page in the panel
vantuly sites                   # ID, type, status, URL
vantuly sites --server web-1
vantuly servers                 # ID, name, status, IP
vantuly help
```

## 6. For AI agents and scripts

- Every command takes `--json`. Errors are printed on stdout as
  `{"error": {"code": "...", "message": "...", "fields": {...}}}`. `code` and `fields` come from
  the panel; local errors (a wrong flag, no linked site) have only `message`.
- No command ever prompts.
- Exit codes are stable:

| Code | Meaning |
|---|---|
| `0` | ok |
| `1` | usage or API error |
| `2` | the deployment ran and failed (`--json` prints the deploy result with `log_tail`) |
| `3` | not signed in, or the token may not do this |

`vantuly deploy dist --json` returns `url`, `site_id`, `created_site`, `detected_type`, `files`,
`bytes`, `deployment` and, on failure, `log_tail`.

### MCP server

```sh
vantuly mcp install claude     # or codex, opencode, cursor
```

| Client | Config written |
|---|---|
| `claude` | `.mcp.json` in this folder |
| `cursor` | `.cursor/mcp.json` in this folder |
| `opencode` | `opencode.json` in this folder |
| `codex` | `~/.codex/config.toml` |

Other entries in the file are kept. The entry runs `npx -y vantuly mcp` (without npx on the
computer, `uvx vantuly mcp`; without either, this binary's own path). Run `vantuly login` first, then restart the agent to load it.
`vantuly mcp` on its own is the server on stdio.

The server runs in the project folder with the CLI's sign-in. Its tools: `whoami`,
`list_servers`, `list_sites`, `deploy`, `deployment_status`, `deployment_logs`, `rollback`,
`add_domain`, `domain_status`, `set_env`, `list_env`. `deploy` only takes folders inside the
project folder: the first `file://` root the agent reports (MCP roots), else the folder the
server was started in. There are no delete, terminal, firewall, backup or team tools.

A prompt to give an agent:

```text
Build this site and deploy it with the vantuly MCP tools (or `npx -y vantuly deploy <folder> --json`).
Deploy the build output folder, not the project. If the deploy fails, read log_tail, fix the cause
and deploy again. Never deploy .env files; set variables with set_env. Tell me the URL at the end.
```

### Connect with a URL (hosted MCP server)

Apps that cannot run a local program (claude.ai, ChatGPT, the Claude mobile app) and agents that
prefer a URL connect to `https://app.vantuly.com/mcp` (a self-hosted panel: `<panel URL>/mcp`).
Signing in happens in the browser: pick the team and confirm your password. The connection has
the same deploy access as `vantuly login` for that one team and is listed under Settings → API
tokens → Connected apps, where it can be revoked.

- Claude Code: `claude mcp add --transport http vantuly https://app.vantuly.com/mcp`
- Codex: `codex mcp add vantuly --url https://app.vantuly.com/mcp`, then `codex mcp login vantuly`
- claude.ai and ChatGPT: add a custom connector with the URL.

The hosted server cannot read your files, so instead of `deploy` it has `prepare_upload`: it
creates the site if needed and returns a one-time upload link (15 minutes) with a ready
`curl -F file=@site.zip ...` command; `site_id` is required by the other site tools.

## 7. Troubleshooting

**`not signed in: run vantuly login (or set VANTULY_TOKEN)`** (exit 3). Run `vantuly login`. A
token that expired (after 90 days) or was revoked in the panel also exits 3; sign in again.

**`This code is wrong, expired or already used`** in the browser, or **`the sign-in expired`** in
the terminal. A code is valid once, for 10 minutes. Run `vantuly login` again and approve the new
code.

**`Confirm your email address first`**. Open the link in the email Vantuly sent you (or send a new
one from your profile), then approve the sign-in.

**`this team has 2 servers: pass --server with one of: ...`**. Pass `--server NAME`. Only the
first deploy of a folder needs it. `this team has no ready server` means you have to add one in
the panel first.

**`this is a astro project: run npm run build, then vantuly deploy .../dist`**. The CLI found a
static-site project. Build it and deploy the folder it names.

**`nothing to deploy in ...`**. The folder has no `index.html`, `package.json`,
`requirements.txt` or `pyproject.toml` at the top. Pass the build output folder.

**`Deploy failed (failed). The live site was kept.`** (exit 2). The CLI prints the last 60 lines
of the build log. Fix the cause and deploy again; `vantuly logs` shows the whole log.

**`site ... is not in this team`**. The folder is linked to another team's site. Remove
`.vantuly/project.json` to create a new site, or sign in to the right team.

**`checksum mismatch for vantuly-...: not installed`**. The download did not match the release's
`checksums.txt`, so nothing was installed. Try again; if it keeps happening, do not work around it,
and tell us.

## 8. Security

- The deploy token is bound to one team and can only do what section 2 lists. It expires after
  90 days; `vantuly logout` revokes it at once, and so does removing it in the panel.
- The sign-in is saved in `~/.config/vantuly/auth.json` with mode 0600 (under
  `$XDG_CONFIG_HOME/vantuly` when that is set). `.vantuly/project.json` holds only the site ID,
  team ID and URL.
- Every deploy appears in the team's activity log, so what a person or an agent did is visible
  and can be rolled back.
