Run local AI CLIs — Claude, Codex, Antigravity (agy) — and any third-party
OpenAI-compatible API as MCP tools, with background jobs. Self-maintained from
source, built on a registry architecture: adding an agent means adding one file.
Requires Node ^20.19.0 || >=22.12.0.
完整中文文件(環境變數、設定檔、每個工具的細節)見 README.zh-TW.md. The Chinese document is the exhaustive reference; this page covers the design and the parts you need to get running.
git clone <repo-url> ai-cli-mcp-source
cd ai-cli-mcp-source
npm install # triggers build via the prepare script and produces dist/
claude mcp add ai-cli -s user -- node "$PWD/dist/server.js"dist/ is not version-controlled, so the build has to run before first use.
npm install runs it through the prepare script, so a separate npm run build
is normally unnecessary.
Each supported CLI becomes a tool you can call from an MCP client. Jobs run in the
background: run returns a PID immediately, and list_processes, peek, wait and
get_result observe it while it works. Three entry points — the MCP server, the
ai-cli command line, and the detached file-backed runner — are equivalent in
behavior; they differ only in where job state lives.
src/
├─ agents/ one file per AI — this is the extension point
│ ├─ types.ts AgentDefinition, the core contract
│ ├─ registry.ts central registry
│ └─ claude.ts · codex.ts · antigravity.ts · direct-api.ts
├─ core/ the framework; untouched when adding an agent
│ ├─ command-builder.ts model routing and command assembly
│ ├─ process-service.ts in-memory job management (MCP)
│ ├─ file-process-service.ts file-backed job management (detached CLI)
│ ├─ pty-runner.ts ConPTY, for CLIs that need a real TTY
│ ├─ circuit-breaker.ts start-rate and duplicate-prompt breaker
│ ├─ updater.ts background check, subprocess apply, rollback
│ └─ user-config.ts · binary-resolver.ts · peek.ts · doctor.ts
├─ models/ model catalog, aliases, provenance and disk cache
├─ plugins/ quota lookup bridge
└─ app/ mcp.ts (MCP server) · cli.ts (command line)
The split is the point: agents/ is data about each CLI, core/ is the machinery.
A new backend never requires editing the machinery.
- Copy
src/agents/codex.tstosrc/agents/<name>.tsand implementAgentDefinition:id,models,matchesModel,binary,reasoning,buildCommand,parseOutput.- Needs a real TTY → set
win32SpawnMode: 'pty'(seeantigravity.ts). - Is a real
.exerather than an npm shim → setwin32DirectExec: true.
- Needs a real TTY → set
- Import it in
src/agents/registry.tsand append it toAGENTS. Claude stays last — it is the fallback. - Add the new id to
AgentIdinsrc/agents/types.ts. If the agent needs a CLI binary, extend theCliPathsreturn incore/doctor.ts. npm run build.
A caller-side bug can turn into an infinite loop, and an infinite loop hammering a
provider can get an account flagged for abuse. Every subprocess start therefore goes
through src/core/circuit-breaker.ts first, which watches for two loop signatures:
- Rate — starts within the sliding window exceed
AI_CLI_BREAKER_MAX_STARTS. - Duplicate — the same agent with the same prompt exceeds
AI_CLI_BREAKER_DUP_LIMITwithin the window.
Either one opens the breaker for AI_CLI_BREAKER_COOLDOWN_SEC, during which starts
are refused with an explicit error, after which it recovers on its own. Normal usage
does not trip it. Verify with npm run build && node verify-breaker.mjs.
A wait timeout means the observation window closed, not that the job failed — the
process keeps running. Earlier versions threw Timed out after N seconds, which MCP
wrapped as an InternalError, and calling models routinely read that as failure and
abandoned the PID.
Now wait returns the array of current results instead. Only items still running
at the timeout carry timedOut: true. An unknown PID is still an error;
completed, failed and lost carry neither liveness nor timedOut.
Running items from get_result, wait and list_processes all carry the same
liveness object:
| Field | Meaning |
|---|---|
alive |
Process is up. It does not promise the model is producing tokens. |
elapsedSec |
Seconds since start. |
sinceLastOutputSec |
Seconds since the last stdout/stderr chunk; null if there has never been output. |
stdoutBytes / stderrBytes |
Bytes received. PTY-merged output counts as stdout. |
lastEvent |
One-line summary of the last meaningful event, ≤120 chars. |
eventCount |
Complete decoded events. Blank lines, bad JSON and partial lines do not count. |
hint |
Plain-English advice: starting up, recently active, alive but silent, or waiting on end-of-run metadata. |
This matters because Codex and Claude emit nothing at all while reasoning. Without
liveness, silence is indistinguishable from death.
The file-backed path keeps a lost state: the PID is gone and no completion was
recorded, so the outcome is genuinely unknown — which is not the same as failed.
All three entry points serve normally after the transport connects, then check in the background ~3 seconds later and apply updates in a subprocess; the new version takes effect on the next start. Nothing blocks startup, and no running server or job is killed.
AI_CLI_AUTO_UPDATE |
Behavior |
|---|---|
on (default) |
Check in the background, apply automatically |
check |
Report a new version, do not apply |
off |
Never touch the network; still reads existing state |
Also honored: AI_CLI_UPDATE_CHECK_INTERVAL_SEC (default 3600),
AI_CLI_UPDATE_BRANCH, and AI_CLI_STATE_DIR (default ~/.local/state/ai-cli).
Any OpenAI-compatible endpoint can be wired in yourself through the direct-api
agent, addressed as or-<model> for OpenRouter, ds-<model> for DashScope, or
<provider>-<model> for any provider key configured in
~/.local/share/ai-cli/providers.json. See the Chinese reference for the config
file format and the limits of what this path can do.
# bash / git bash
claude mcp add ai-cli -s user -- node "$PWD/dist/server.js"# PowerShell
claude mcp add ai-cli -s user -- node "$PWD\dist\server.js"Apache-2.0. See LICENSE.