agy-mcp is a layered bridge between an agentic caller (Claude Code,
OpenAI Codex) and Google Antigravity's agy CLI. Each layer is in its
own module so a future maintainer can replace one without touching the
others.
+----------------------------------------------------------------+
| Caller (Claude Code / Codex) |
| - SKILL.md tells the agent WHEN to delegate |
| - Receives a JSON envelope (BridgeResponse) on every call |
+--------------------------+-------------------------------------+
|
MCP stdio (FastMCP)
|
+--------------------------v-------------------------------------+
| MCP server (src/agy_mcp/server.py) |
| - 11 tools: agy, agy_start, agy_continue, agy_status, |
| agy_read, agy_result, agy_cancel, agy_sessions, |
| agy_doctor, agy_install_skill, agy_purge |
| - Singletons: config, safety, session_store, supervisor |
| - Async tools wrap a per-loop CapacityLimiter (8 by default) |
+--------------------------+-------------------------------------+
|
+--------------+----------------+
| |
+-----------v-----------+ +------------v-----------+
| Bridge CLI | | Supervisor |
| (bridge.py) | | (supervisor.py) |
| | | |
| - argparse front-end | | - Thread per job |
| - Reads/validates | | - Semaphore-bound |
| BridgeRequest | | concurrency cap |
| - Calls adapter.run() | | - Cross-platform |
| synchronously | | process group |
| - Emits BridgeResponse| | cancel (POSIX killpg |
+-----------+-----------+ | / Windows BREAK) |
| +-------------+----------+
| |
+--------------+-----------------+
|
+--------------------------v-------------------------------------+
| Adapter layer |
| - base.py : BaseAdapter, AdapterRunResult, EventSink |
| - agy.py : AgyPrintBackend (Hybrid: stdout + log tail) |
| - gemini.py: GeminiCliBackend (compat fallback) |
| - protocol.py: ProtocolTranslator (raw / claude / codex) |
+--------------------------+-------------------------------------+
|
subprocess (POpen)
|
+--------------------------v-------------------------------------+
| External binary |
| - agy (Google Antigravity CLI) |
| - or gemini (fallback when agy is not on PATH) |
+----------------------------------------------------------------+
Every error string, log message, and response field is run through
SafetyPolicy.redact() before leaving the process. The policy:
- Recognises PEM blocks, JWT tokens, AWS access key IDs, Bearer / Authz
headers, and anonymises absolute home-directory paths (
/Users/<u>/→~/,/home/<u>/→~/,C:\Users\<u>\→~\). - Caches compiled patterns; the cache is RLock-guarded so two MCP tool invocations cannot race on first use.
- Rejects destructive prompts (
rm -rf,mkfs,dd if=/dev/zero, pattern-based) even whenallow_write=True.
Resolution precedence (highest wins):
- Tool / CLI flag argument.
- Environment variable (
AGY_MCP_WORKTREE_DEFAULT,AGY_MCP_BACKEND,AGY_MCP_OUTPUT_PROTOCOL). ~/.config/agy-mcp/config.toml(or%APPDATA%/agy-mcp/config.toml).- Built-in defaults.
AGY_CLI_DISABLE_AUTO_UPDATE is not part of this resolution chain
— it is a subprocess passthrough set by adapters/agy.py into the
child's environment so the agy CLI does not phone home during a
build, and it has no effect on agy-mcp's own behaviour.
Defaults: worktree=True for execute mode, allow_write=False,
backend="auto", output_protocol="claude".
<root>/<job_id>/
meta.json # JobRecord (status, timing, redacted command)
events.jsonl # CanonicalEvent stream (one JSON object per line)
stdout.log # raw agy stdout
stderr.log # raw agy stderr
agy.log # agy --log-file (klog) — lifecycle events
artifacts/ # files emitted by execute mode
<root> defaults to ~/.agy-mcp/sessions/ and is mkdir(0o700) on
first use. Every write goes through safe_write_text(verify_under=root)
to defeat parent-symlink swaps.
When mode=execute and allow_write=True, the bridge creates a git
worktree at <repo>/.agy-mcp/worktrees/<session_id>/ and runs the child
process with cwd set to that worktree. The worktree remains after the
run so the caller can inspect, merge, or discard the branch. Falls back
to running directly in the repo only when the user has worktree=False
set; otherwise refuses.
The single write primitive used by session_store, install, and
worktree. With verify_under on POSIX platforms that expose the
required openat APIs, it pins the resolved root with
O_DIRECTORY|O_NOFOLLOW, walks intermediate directories via dir_fd
with O_NOFOLLOW, creates a random tempfile under the pinned parent
fd, and renames it with src_dir_fd / dst_dir_fd. Windows and
openat-unavailable filesystems use the older pre-write and post-rename
parent walk with relative_to(verify_under) checks; that fallback
surfaces a successful parent-swap race but cannot undo a published leaf.
class BridgeRequest(BaseModel):
prompt: str # 1 ≤ len ≤ 256_000
cwd: str = "."
session_id: str | None = None
model: str | None = None
sandbox: bool = False
mode: Literal["ask","plan","prototype","review","execute","browser","long"] = "ask"
return_all_messages: bool = False
timeout: int = 900 # 1 ≤ value ≤ 86400 (24h)
detach: bool = False
allow_write: bool = False
worktree: bool | None = None # None -> use config default
max_output_chars: int = 60000 # 1 ≤ value ≤ 8 MiB
session_id: str | None = None # max 96 chars
backend: Literal["auto", "agy", "gemini"] = "auto"
output_protocol: Literal["raw", "claude", "codex"] = "claude"
extra_env: dict[str, str] = {} # validated: ^[A-Z_][A-Z0-9_]*$,
# value cannot contain \n / \r / NUL,
# no runtime-control names,
# max 64 entries, max 4096 chars each
debug: bool = False
class BridgeResponse(BaseModel):
success: bool
SESSION_ID: str = ""
job_id: str | None = None
status: Literal["completed","running","failed","cancelled","unknown","upstream_error"] = "unknown"
agent_messages: str | list[dict] = ""
all_messages: list[dict] = []
artifacts: list[dict] = []
error: str | None = None
warnings: list[str] = []
cwd: str = ""
# Structured metadata: backend, bin_path, version, capability matrix,
# extra fields (extra="allow" so backends can attach their own keys).
# See ``src/agy_mcp/models.py::AdapterMetadata``.
adapter: AdapterMetadata = AdapterMetadata()
command_preview: list[str] | None = None # debug/dry-run only
log_path: str | None = None
created_at: str = ""
updated_at: str = ""Both models use Pydantic v2 (extra="forbid" on request, extra="allow"
on canonical events so future agy event types survive without a schema bump).
| Tool | Sync | Purpose |
|---|---|---|
agy |
yes (async-wrapped) | One-shot synchronous call; standard PROMPT / cd / sandbox / SESSION_ID arg set + new mode/backend/output_protocol/worktree/allow_write/extra_env |
agy_continue |
yes (async-wrapped) | Resume a prior SESSION_ID |
agy_start |
no | Spawn a background job; returns job_id immediately |
agy_status |
yes | running / completed / failed / cancelled / upstream_error + timing |
agy_read |
yes | Stream events (raw or protocol-translated) + artifacts |
agy_result |
yes | Return the captured output for a finished background job |
agy_cancel |
yes | Process-group cancel (POSIX killpg / Windows CTRL_BREAK_EVENT) |
agy_sessions |
yes | List recent jobs with mtime / status / cwd summary |
agy_doctor |
yes | Environment + auth probe (no secrets); force_refresh=True after CLI upgrade |
agy_install_skill |
yes | Install SKILL bundle into Claude / Codex / Antigravity dirs |
agy_purge |
yes | Prune session-store directories older than days (refuses days<=0) |
Sync tools are declared async def so they yield the FastMCP stdio loop
while doing blocking I/O, but they internally run the bridge in a worker
thread via anyio.to_thread.run_sync. A per-loop CapacityLimiter
caps concurrent calls at 8.
Every non-trivial code path returns through _structured_failure →
BridgeResponse so callers always get a success=False envelope rather
than an exception. _structured_failure runs the error message through
SafetyPolicy.redact so paths and tokens are anonymised before they
land in the caller's transcript.
docs/installation.md— how to install and register.docs/output-strategy.md— adapter / event-synthesis / protocol-translator detail.docs/security.md— threat model and what is / is not defended.docs/cli-capabilities.md— whatagy --helpactually reports (refreshed when the CLI is updated).docs/examples.md— six end-to-end usage scenarios.