antigravity-cli
by LoliLin
README.md
# memoh-antigravity-cli-mcp
An MCP server that lets a chat agent drive the **official Antigravity CLI** (`agy`) headlessly:
run a task in a separate agent context, resume it later, inspect what it actually did, and get
through Google sign-in on a machine with no browser.
Built for [Memoh](https://app.memoh.net) workspaces, usable from any MCP client.
```
you ──> Memoh agent ──> [MCP stdio] ──> agy -p --output-format stream-json ──> agent works
▲ │
└────────────── ok / status / response / tool_calls ◄────────────────┘
```
---
## Why this exists
`agy` is perfectly usable from a shell. Two things bite you the moment you wrap it in an MCP
server, and both are handled here:
**1. Interactive login cannot be done from print mode.**
`agy -p` waits for authentication with a hard-coded **60 second** limit, then exits with
`authentication timed out`. The OAuth URL appears in that window — far too short to hand it to a
human, and it dies before the authorization code comes back. The interactive sign-in runs the
*same* OAuth flow with **no such limit**, so this bridge runs that one process inside a tmux
session. The PKCE verifier lives in that process, which is why the login is split into
`agy_login_start` / `agy_login_submit` and survives between the two calls.
**2. Headless mode ignores `permissions.allow`, and denied runs report success.**
Tracked upstream as [issue #548](https://github.com/google-antigravity/antigravity-cli/issues/548).
With permissions left alone, headless `agy` cannot show an approval prompt, so **every** file read
or command is auto-denied — while the run still returns `SUCCESS` and exit code 0. The denial is
only visible in `denied_hints`. This bridge surfaces `permission_mode`, `denied_hints` and
`tool_calls` on every result so "did nothing" can never be mistaken for "did the work".
---
## Requirements
| | |
|---|---|
| Node | 18 or newer (no dependencies) |
| `agy` | official Antigravity CLI, installed **and signed in** |
| `tmux` | only for the sign-in flow; not needed for `agy_run` |
Install the CLI, then check it:
```sh
curl -fsSL https://antigravity.google/cli/install.sh | sh # see the official docs for the current one-liner
export PATH="$HOME/.local/bin:$PATH"
agy --version
```
## Install
```sh
git clone git@github.com:LoliLin/memoh-antigravity-cli-mcp.git
cd memoh-antigravity-cli-mcp
# Optional: bundle tmux + its shared libraries into ./bin/ so the sign-in flow
# works even when the MCP host runs in a container without tmux on PATH.
sh scripts/vendor-tmux.sh
```
There is no build step and no `npm install` — the server is plain ESM on the Node standard library.
## Register it
Generic MCP config:
```json
{
"mcpServers": {
"antigravity-cli": {
"command": "node",
"args": ["/abs/path/to/memoh-antigravity-cli-mcp/server.mjs"],
"cwd": "/abs/path/to/memoh-antigravity-cli-mcp",
"env": { "AGY_DEFAULT_CWD": "/path/to/your/workspace" }
}
}
}
```
In Memoh: **Settings → MCP → add a stdio server**, with `node` as the command and the absolute
path to `server.mjs` as its argument. Then probe the connection — the ten `agy_*` tools should
appear.
### Environment
| Variable | Meaning |
|---|---|
| `AGY_DEFAULT_CWD` | working directory for runs when a call omits `cwd`. Defaults to the server's own working directory. |
| `AGY_BIN` | explicit path to the `agy` binary, if it is not on `PATH`. |
## Sign in
Ask the agent for `agy_status` first. If `auth` is not `authenticated`:
1. `agy_login_start` → returns the Google OAuth URL.
2. Open it in a browser, sign in **with your own account**, and copy the authorization code.
3. `agy_login_submit(code="…")`.
4. `agy_status(probe_auth=true)` to confirm.
Sign in with your own account and stay inside your own quota. If `agy_login_start` says tmux is not
usable, the `path` and `error` in its reply tell you why — running `scripts/vendor-tmux.sh` is
usually the fix.
## Tools
| Tool | Purpose |
|---|---|
| `agy_status` | binary, version, auth state, resolved tmux, known sessions. Call this first. |
| `agy_run` | run a prompt in a **new** conversation. |
| `agy_continue` | follow up in an existing conversation (`conversation_id`, `label`, or `continue_last`). |
| `agy_sessions` | list remembered label → conversation mappings. |
| `agy_models` | models available to the signed-in account. |
| `agy_agents` | named agents exposed by the CLI. |
| `agy_login_start` | begin sign-in, return the OAuth URL, keep the session alive. |
| `agy_login_submit` | feed the authorization code back into the waiting process. |
| `agy_login_cancel` | abort a pending sign-in. |
| `agy_auth_help` | explain the sign-in paths and where config lives. |
Common `agy_run` arguments:
- `prompt` (required) — self-contained; the delegate does **not** see your chat history.
- `cwd` — narrowest directory containing the work.
- `mode` — `"plan"` (read-only) or `"accept-edits"`.
- `timeout_seconds` — set it explicitly; analysis often needs minutes.
- `model`, `label`, `skip_permissions`, `json_schema`, `add_dirs`, `sandbox`, `effort`, `agent`.
## Usage
```
"Have antigravity look at why this repo fails to build, then report back."
→ agy_run(prompt="…", cwd="/path/to/repo", mode="accept-edits", timeout_seconds=900, label="build-fix")
"Ask it to follow up on the same thing."
→ agy_continue(prompt="…", session="build-fix")
```
A conversation is not tied to the server process: each call is a fresh `agy` process that resumes
the stored conversation by id (official `--conversation` / `--continue`). Restarting the MCP server,
or the host, does not lose it.
## Design notes
- **One process per call, resumable sessions.** Conversations live in `agy`'s own store, so a crash
cannot take a session with it, and every run leaves a raw `.ndjson` transcript (`log_path`).
- **Terminal driver only where it is unavoidable.** `tmux` is used *only* to hold an interactive
sign-in open. Everything else is headless `stream-json`.
- **The result is parsed, not trusted.** `tool_calls` is reassembled from `step_update` events
(ACTIVE carries the arguments, DONE carries the output) so you can audit what was run rather than
reading a summary that might contradict it.
### The compliance boundary
This bridge deliberately stays *outside* the CLI, and should stay that way:
- Every action is the official `agy` binary with documented flags (`agy -p … --output-format
stream-json`, `agy models`, `agy --version`) or the normal interactive sign-in UI.
- It never contacts Google endpoints directly and never performs its own OAuth token exchange.
- It never reads, writes, moves or copies `agy` credential files. Sign-in is driven through `agy`'s
own UI, so the token stays `agy`'s.
- It sets no undocumented environment variables (only `PATH`, `TERM`, `NO_COLOR`).
- Quotas and account limits are the signed-in user's own. Working around them is out of scope.
## Permissions
`skip_permissions` maps to the vendor's own documented `--dangerously-skip-permissions` flag, and
means the delegate will not stop to ask before running commands or editing files in `cwd` and below.
- It is **ON by default** here, because headless runs are otherwise auto-denied (see issue #548).
Pass `skip_permissions: false` for a run that must not touch anything.
- Every result echoes `skip_permissions` and a `permissions:` line, so the grant is never silent.
- Point `cwd` at the narrowest directory that contains the work, not at a home directory.
- Prefer `mode="plan"` when you only want analysis.
## Testing
```sh
# Protocol + status smoke test. Needs no agy and no auth.
node test/harness.mjs
# Adds a real agy_run plus an auth probe. Requires a signed-in agy.
node test/harness.mjs --run
# Two unrelated processes resuming one conversation (the persistence claim).
sh test/persist-probe.sh [DIR]
```
The harness speaks JSON-RPC to `server.mjs` over stdio exactly like an MCP client does.
The login flow can be exercised too: `--login-start`, `--login-submit <code>`, `--login-status`.
## Layout
```
server.mjs MCP tool definitions + stdio transport
lib.mjs agy invocation, stream-json parsing, session store
tmux.mjs pty driver for the interactive sign-in
scripts/vendor-tmux.sh bundle tmux + libs into ./bin/ (gitignored)
test/harness.mjs JSON-RPC smoke tests
test/persist-probe.sh cross-process conversation resume check
skills/antigravity-delegation/ agent-facing skill describing when and how to delegate
runs/, bin/, state.json runtime state; all gitignored
```
## Known limitations
- The sign-in TUI is interactive by nature. If the tmux session is killed mid-flow, the PKCE
verifier goes with it and you start over with a new URL.
- Resuming a long conversation replays the transcript, so input tokens (and latency) grow with
conversation length. This is inherent to `--conversation` resume.
- `runs/` and `state.json` grow over time; clean them up on your own schedule.
- Permission handling depends on upstream issue #548. Once `permissions.allow` is honoured in
headless mode, the default here should be revisited.
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues