Skip to main content
Glama
README.md
# polyagent-mcp

MCP server that lets **any** agent or MCP host delegate to headless **Codex, Grok, Claude Code, and
OpenCode CLIs**, with Cursor available as an opt-in fallback. Use the fleet for implementation, planning,
and project exploration without burning the caller's context on raw worker output.

Worker tools take optional **model** and **effort** overrides, return a `session_id`, and support
`follow_up`. Difficulty levels select a distinct default model across the three active
subscriptions; `delegate` also accepts an explicit engine, including pay-per-token OpenCode.

## Tools

The server exposes ten tools:

| Tool | Purpose |
|------|---------|
| `delegate` | Run a task with full **read/edit/shell** access in `cwd`. Required `level`: 1=GPT-5.6 Luna max (codex), 2=GPT-5.6 Sol xhigh (codex), 3=Grok 4.6 high (grok), 4=GPT-6 Astra max (codex), 5=Claude Fable 5.1 max (claude). **Levels 4 and 5 are expensive — 5 by far the most; last resort only.** Optional `engine` overrides the tier; `opencode` requires a `provider/model` model. Optionally accepts an `agent` persona by name or inline `{prompt}`. |
| `fast_delegate` | Same full **read/edit/shell** access as `delegate`, but with no `level` to pick: it routes to whichever CLI is currently the fastest **and** healthy. Prefer it over `delegate` when the task is simple or urgent and picking a level is not worth it. First candidate is subscription (codex luna low); pay-per-token OpenRouter (mercury-2) is the 2nd fallback. Optionally accepts an `agent` persona. |
| `explore` | Read-only exploration on Codex with `gpt-5.6-luna`. `question` alone → broad fan-out search returning `file:line` refs; `question`+`files` → answer about those files; neither → general project map. `breadth: "thorough"` sweeps wider. Locates, does not review. |
| `read_slice` | Surgical read-only read: returns ONLY the code relevant to `want` (exact lines with `file:line`) from the given `files` — the full file never enters your context. Use instead of reading large files whole. |
| `run_filtered` | Run a shell `command` with full access and get back ONLY the lines relevant to `want` — semantic filtering of huge build/test/log output. Default engine is the same FAST_CANDIDATES cascade as `fast_delegate` (codex luna low first; may spend OpenRouter credit if it falls through to mercury-2). |
| `web_lookup` | Web/docs lookup through Codex/Luna with real web search enabled and a read-only filesystem. |
| `generate_image` | Generate or edit an image through Codex's built-in image tool and save it inside `cwd`. |
| `fan_out` | Run the SAME prompt across N engines/tiers in parallel isolated sandboxes and get back ONLY a compact digest — `mode: "race"` (default) returns the first success, `mode: "consensus"` compares every output through one cheap arbiter. |
| `follow_up` | Continue a prior session by `session_id`. |
| `bridge_stats` | Report calls and chars returned to context per tool (needs `POLYAGENT_LOG`). |

Worker tools accept `cwd`, `model`, and `effort` where applicable. `delegate` requires a **level**
(1-5); `fast_delegate` has none and picks the fastest healthy engine. Explicit `model`/`effort`
values override the selected tier.

### When an engine runs out of quota

A call that fails because the engine's plan quota is exhausted does **not** silently retry on
another engine — spending the next subscription is your decision. The call fails with an actionable
error naming the engines still available (installed, enabled, and capable of what that tool needs)
and how to switch: `engine:"<x>"` on the four auxiliary tools and `delegate`, or the lowest
still-usable `level:<n>` when a tier engine remains. Tools that pick the engine themselves (`fast_delegate`, `fan_out`) and `follow_up`
(pinned to the resumed session's engine) report the quota without suggesting a parameter, and
`generate_image` reports it against the two engines that have an image tool at all (codex, grok). A transient rate limit is reported separately and asks you to wait, since switching
engines would not help. Anything the classifier does not recognize — an expired login, for one —
propagates as the raw CLI failure instead of being guessed at.

## Requirements

- Node ≥ 18
- `bubblewrap` (`bwrap`) installed — **required**, not recommended: the sandbox is mandatory and the
  server refuses to start without it (`sudo apt install bubblewrap`). Only `POLYAGENT_SANDBOX=off`
  waives it, as an explicit operator choice.
- Codex installed and authenticated for read tools and levels 1/3; Grok for levels 2/4; Claude Code
  for level 5.
- Optional Cursor fallback: install `cursor-agent` and set `POLYAGENT_ENABLE_CURSOR=1`.

## Install

> **Installing via an AI agent?** Point it at [`INSTALL.md`](INSTALL.md) — an agent-facing,
> copy-paste guide that detects the host and registers the bridge in Claude Code, Cursor,
> Codex, Grok, or any generic MCP host.

```bash
git clone https://github.com/JaimeJunr/polyagent-mcp.git
cd polyagent-mcp
npm install
npm run build
```

## Register in an MCP host

**Claude Code:**
```bash
claude mcp add polyagent -s user -- node /abs/path/to/polyagent-mcp/dist/index.js
```

**Any host** — add to its `mcp.json`:
```json
{
  "mcpServers": {
    "polyagent": {
      "command": "node",
      "args": ["/abs/path/to/polyagent-mcp/dist/index.js"]
    }
  }
}
```

**Permissions (Claude Code):** `claude mcp add` registers the server but does **not** grant
tool permission — without an allowlist every bridge call prompts for approval. After
registering, add either `"mcp__polyagent__*"` (full; also auto-approves mutating tools
`delegate`/`fast_delegate`/`run_filtered`/`follow_up`) or a read-only subset
(`explore`/`read_slice`/`web_lookup`/`bridge_stats`) under
`permissions.allow` in `settings.json`. Full options and trade-offs:
[INSTALL.md §3](INSTALL.md#3-allowlist-the-bridge-tools-claude-code). Cursor/Codex/other hosts
have their own approval settings — consult the host.

## Configuration (env)

| Var | Default | Meaning |
|-----|---------|---------|
| `POLYAGENT_CURSOR_BIN` | `cursor-agent` | Path to the optional Cursor CLI fallback. |
| `POLYAGENT_GROK_BIN` | `grok` | Path to the Grok CLI. |
| `POLYAGENT_CODEX_BIN` | `codex` | Path to the Codex CLI. |
| `POLYAGENT_CLAUDE_BIN` | `claude` | Path to the Claude Code CLI. |
| `POLYAGENT_OPENCODE_BIN` | `opencode` | Path to the OpenCode CLI used by explicit engine selection. |
| `POLYAGENT_ENABLE_CURSOR` | _(off)_ | Set to `1`/`true` to allow Cursor fallback when a tier's preferred CLI is missing. Otherwise the call fails with the missing CLI named. |
| `POLYAGENT_MODEL` | `composer-2.5-fast` | Default model for the optional Cursor path. |
| `POLYAGENT_EXPLORE_MODEL` | `gpt-5.6-luna` | Codex model for `explore`, `read_slice`, and `web_lookup` when neither the call nor the tool-specific `_MODEL` sets one. `run_filtered` defaults to the `fast_delegate` cascade, not this. |
| `POLYAGENT_<TOOL>_ENGINE` | `codex` (read tools) | Per-tool engine for the four auxiliary tools — `<TOOL>` is `EXPLORE`, `READ_SLICE`, `RUN_FILTERED`, or `WEB_LOOKUP`. The call's own `engine` parameter beats it. Unset `RUN_FILTERED` falls through to the `fast_delegate` cascade, not hardcoded codex. Refused when the engine lacks what the tool needs: read-only (`explore`/`read_slice`/`web_lookup`, which outside codex comes from the sandbox) or web search (`web_lookup`, codex only). |
| `POLYAGENT_<TOOL>_MODEL` | _(see above)_ | Per-tool model, same four names. The call's `model` beats it. With a non-codex engine and no model set anywhere, the engine's own default model is used. |
| `POLYAGENT_AGENT_PATHS` | _(off)_ | Additional `:`-separated roots for named agent personas, searched before project/home `.claude/agents` and `~/.claude/plugins`. |
| `POLYAGENT_SANDBOX` | `bwrap` | Isolates every engine in a bubblewrap sandbox with an empty `$HOME`, preventing global config, MCP servers, hooks, and skills from loading. Only auth, required engine state, and toolchains are bound in. Set `off`/`0` to disable explicitly — with the sandbox off, the read-only tools (`explore`, `read_slice`, `web_lookup`) accept only the codex engine. A missing `bwrap` is a startup error, never a silent downgrade. |
| `POLYAGENT_FORCE` | _(off)_ | If `1`/`true`, force-enable non-interactive approval for Cursor, Claude, and OpenCode runs. |
| `POLYAGENT_TIMEOUT_MS` | `1800000` (30 min) | Per-call safety-net timeout (not a work budget). Execution tools (`delegate`/`fast_delegate`) also get a prompt note so the worker returns partial results before being killed. |
| `POLYAGENT_LOG` | _(off)_ | Path to a JSONL file; when set, every call logs `{tool, outChars}` for `bridge_stats`. |
| `POLYAGENT_HOOK_MODE` | `redirect` | Hook behavior: `off` (no-op), `nudge` (non-blocking `additionalContext` only), or `redirect` (deny once + name bridge tool for WebSearch/WebFetch and whole-file large Read; fail-open on retry). Grep/Glob/Bash/Edit/Write stay nudge-only. |
| `POLYAGENT_HOOK_MIN_LINES` | `300` | Line threshold above which the optional hook (below) redirects/nudges whole-file Read toward `read_slice`. |

### Breaking change: env var rename

Every `CURSOR_BRIDGE_*` variable was renamed to `POLYAGENT_*` (same suffix), and `CURSOR_BIN`
became `POLYAGENT_CURSOR_BIN`. This is a **clean cut**: the old names are no longer read at all —
setting one has zero effect (no fallback, no warning). Update your host config (`mcp.json` /
`settings.json` `"env"` blocks) and any shell profile before upgrading.

| Old (removed) | New |
|---------------|-----|
| `CURSOR_BIN` | `POLYAGENT_CURSOR_BIN` |
| `CURSOR_BRIDGE_AGENT_PATHS` | `POLYAGENT_AGENT_PATHS` |
| `CURSOR_BRIDGE_GROK_BIN` | `POLYAGENT_GROK_BIN` |
| `CURSOR_BRIDGE_CODEX_BIN` | `POLYAGENT_CODEX_BIN` |
| `CURSOR_BRIDGE_CLAUDE_BIN` | `POLYAGENT_CLAUDE_BIN` |
| `CURSOR_BRIDGE_MODEL` | `POLYAGENT_MODEL` |
| `CURSOR_BRIDGE_EXPLORE_MODEL` | `POLYAGENT_EXPLORE_MODEL` |
| `CURSOR_BRIDGE_IMAGE_MODEL` | `POLYAGENT_IMAGE_MODEL` |
| `CURSOR_BRIDGE_FORCE` | `POLYAGENT_FORCE` |
| `CURSOR_BRIDGE_ENABLE_CURSOR` | `POLYAGENT_ENABLE_CURSOR` |
| `CURSOR_BRIDGE_TIMEOUT_MS` | `POLYAGENT_TIMEOUT_MS` |
| `CURSOR_BRIDGE_DEBUG` | `POLYAGENT_DEBUG` |
| `CURSOR_BRIDGE_SANDBOX` | `POLYAGENT_SANDBOX` |
| `CURSOR_BRIDGE_SANDBOX_EXTRA` | `POLYAGENT_SANDBOX_EXTRA` |
| `CURSOR_BRIDGE_LOG` | `POLYAGENT_LOG` |
| `CURSOR_BRIDGE_HOOK_MODE` | `POLYAGENT_HOOK_MODE` |
| `CURSOR_BRIDGE_HOOK_MIN_LINES` | `POLYAGENT_HOOK_MIN_LINES` |

"cursor" survives only where it names the actual Cursor engine (`POLYAGENT_CURSOR_BIN`,
`POLYAGENT_ENABLE_CURSOR`).

> **Security:** `delegate`, `fast_delegate`, and `run_filtered` have full access and auto-approve
> their work. `explore`, `read_slice`, and `web_lookup` use Codex's read-only sandbox. Named agents
> are resolved on the host, reject path traversal, and are injected without mounting agent
> directories.

## Make the agent actually use it

Registering the tools is not enough. Two structural forces push the agent back to
native tools: (1) the host rule "prefer the dedicated file/search tools", and (2) MCP
tools used to be **deferred** — the agent had to run a tool-search to load their schemas,
so always-loaded `Read`/`Grep`/`WebSearch` won by default. The server now publishes
**startup `instructions`** (routing boundary) and marks the five core tools with
`_meta: { "anthropic/alwaysLoad": true }` (Claude Code ≥2.1.121) so their schemas load
eagerly. `fast_delegate` joined them for the same reason: deferred, it was never picked.
The remaining secondary tools stay deferred. Four fixes, strongest first:

**1. Call-time hook (recommended).** A `PreToolUse` hook that steers the agent toward
the bridge at the moment it reaches for a native tool — text in a config file loses under
pressure, a call-time reminder does not. This repo ships one at
[`hooks/prefer-polyagent.mjs`](hooks/prefer-polyagent.mjs): it runs on `node`
(already required) and only fires where it pays. Default mode is **`redirect`**
(`POLYAGENT_HOOK_MODE=redirect`): for the two safe-to-block cases it returns
`permissionDecision: "deny"` once and names the bridge tool; other cases stay non-blocking
nudges. Wire it into your host's settings (Claude Code `settings.json`):

> **Breaking change (US-007):** The hook file was renamed from
> `hooks/prefer-cursor-bridge.mjs` to `hooks/prefer-polyagent.mjs`. Update any host
> `settings.json` entry that points to the old path.

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Grep|Glob|WebSearch|WebFetch|Bash|Edit|Write|MultiEdit",
        "hooks": [
          { "type": "command", "command": "node /abs/path/to/polyagent-mcp/hooks/prefer-polyagent.mjs", "timeout": 5 }
        ]
      }
    ]
  }
}
```

What it emits, and when — each fires **at most once per session** (deduplicated in a tmp
file keyed by `session_id`), because a repeated fire is worse than none: the agent learns
to ignore it *and* every fire costs tokens. Dedup keys are saved **before** emitting so
redirect is one-shot and fail-open (a second identical call is allowed through).

> - **`Read`** whole-file (no offset/limit) over `POLYAGENT_HOOK_MIN_LINES` lines →
>   **redirect** (default) or nudge toward `read_slice` (once per file). Partial reads are left alone.
> - **`WebSearch`/`WebFetch`** → **redirect** (default) or nudge toward `web_lookup` (once).
> - **`Grep`/`Glob`** → emits the one-time **preload** reminder to run the `ToolSearch` for any
>   still-deferred bridge tools (nudge only — never redirected). The dedup collapses them to a
>   single fire.
> - **`Bash`** whose command writes an artifact (`git commit`/`push`, `git worktree add`,
>   `gh pr create`, `gh issue create`, `bkt pr create`) → suggests offloading that grunt-work to
>   `delegate` (once, nudge only). Read-only Bash (status/diff/log/checkout) is left alone — the
>   orchestrator needs that state, and a mechanical filter (e.g. rtk) already trims the noise.
> - **`Edit`/`Write`/`MultiEdit`** → once per session, reminds that a *self-contained* task
>   (feature, bugfix, mechanical multi-file change, build fix) can go **whole** to `delegate(prompt, level)`
>   — the selected worker edits with full access — instead of the orchestrator implementing
>   it on expensive tokens. It never blocks the edit (nudge only); the once-per-session dedup means
>   the orchestrator still edits inline freely (the nudge repositions execution, it doesn't police every edit).
> - The **first** qualifying fire of the session (whichever tool triggers it) also carries
>   that preload reminder, so secondary schemas get loaded even in a Read-only or web-only session.
> - Redirect deny reasons end with a fail-open suffix: if the bridge tool isn't loaded yet, run
>   ToolSearch first; if the native tool is genuinely needed, call it again and it will be allowed
>   (critical under headless `-p` so the agent never hard-stalls).

Set `POLYAGENT_HOOK_MODE=nudge` for the old non-blocking behavior, or `off` to disable.
To reset the dedup and see the fires again, start a new session (or delete
`polyagent-nudged-<session_id>.json` from your OS temp dir — `os.tmpdir()`,
e.g. `/tmp` on Linux, not necessarily `$TMPDIR`).

### Preloading at session start (`SessionStart`)

The PreToolUse preload above only fires when the agent uses the **`Grep`/`Read`** tool. But
under pressure agents often reach for **`Bash grep`** instead, which matches no PreToolUse
matcher — so the preload reminder never arrives. Wire the same hook for `SessionStart` to
close that hole: the preload reminder then lands in context **before the first tool decision**,
regardless of how the agent searches.

```json
{
  "hooks": {
    "SessionStart": [
      { "hooks": [{ "type": "command", "command": "node /abs/path/to/polyagent-mcp/hooks/prefer-polyagent.mjs", "timeout": 5 }] }
    ]
  }
}
```

On `SessionStart` the hook emits the `ToolSearch` preload as `additionalContext` and pre-marks
`preload` as seen in the session's dedup file, so the PreToolUse piggyback never repeats it.

### Reaching subagents too (`SubagentStart`)

The nudges above only steer the **main loop**. Spawned subagents never see them,
so wire the same hook for `SubagentStart` as well:

```json
{
  "hooks": {
    "SubagentStart": [
      {
        "hooks": [
          { "type": "command", "command": "node /abs/path/to/polyagent-mcp/hooks/prefer-polyagent.mjs" }
        ]
      }
    ]
  }
}
```

On `SubagentStart` the hook injects a compact polyagent preference into every
spawned subagent via `additionalContext` (`subagentStartContext(agent_type)`).
When `agent_type` is `Explore` it appends an extra line: that Explore run was spawned on the
orchestrator's expensive model (Explore inherits the session model, capped at Opus), so it should
route **all** reading through `explore`/`read_slice` (which run on Codex Luna) and
keep the expensive shell to orchestration only.

> **Coexisting with context-mode.** The bridge and context-mode use separate channels
> (context-mode may still do its own thing; this hook only emits `additionalContext`),
> so they coexist cleanly — no `updatedInput` race, no delay, no import of
> context-mode's routing.

**2. Preload any still-deferred tools.** The five core tools and `fast_delegate` are already `alwaysLoad` on
Claude Code ≥2.1.121. For secondary tools (or older hosts), tell the agent to load schemas
once per session. Add to your `CLAUDE.md`/`AGENTS.md`:

```
At the start of any session involving code reading/exploration, run tool-search once for
`read_slice, explore, run_filtered, web_lookup` (and any secondary bridge tools you need) so
their schemas are loaded if the host still defers them.
```

**3. Reconcile the conflict in `CLAUDE.md`.** State the precedence explicitly:

```
The host rule "prefer dedicated file/search tools" applies to the EDIT path (Edit needs the
file content → native Read). For PURE reading/locating/web (no edit), polyagent takes
precedence over native Read/Grep/Glob/WebSearch/WebFetch. Read a large file whole with native
Read ONLY when you are about to edit it.
```

**4. Delegate execution, not just exploration.** The bridge is not only for reading — `delegate`
runs implementation work with full read/edit/shell access, so the orchestrator shouldn't burn its
own tokens on self-contained tasks. State this in `CLAUDE.md` so the agent routes *doing*, not just
*finding*, to the cheap worker:

```
You are the ORCHESTRATOR. delegate(prompt, level) is the DEFAULT for BOTH execution AND judgment.
`level` picks a distinct tier: 1=GPT-5.6 Luna max (codex), 2=GPT-5.6 Sol xhigh (codex), 3=Grok 4.6
high (grok), 4=GPT-6 Astra max (codex), 5=Claude Fable 5.1 max (claude). Levels 4 and 5 are expensive
(5 the most by far) — reserve them for what cheaper levels cannot do. The worker has full read/edit/shell access
in cwd when you delegate. The constant win is context economy: the worker's raw output never enters
your context. Delegate it, then review the result; edit inline only for a quick one-off you're
already positioned for. Use fast_delegate(prompt) when the work is self-contained and you just want
the fastest healthy worker. Pass agent:"name" or agent:{prompt:"..."} when the worker
needs a specialized persona.
```

## Develop

```bash
npm test       # vitest — unit tests for model resolution / arg building
npm run dev    # run from source via tsx
```

## License

MIT

TDQS

A3.9/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: delegate is for task execution, explore for read-only exploration, and follow_up for continuing sessions. No overlap or ambiguity.

Naming Consistency5/5

All tool names use verb-style in lowercase, with follow_up using an underscore for the two-word name. The pattern is consistent and predictable.

Tool Count5/5

With 3 tools covering core operations (task delegation, codebase exploration, session continuation), the count is well-scoped for a bridge server and neither too sparse nor excessive.

Completeness4/5

The tool set covers the primary interactions with the Cursor CLI agent. A minor gap is the lack of session listing or cancellation, but the essential workflows are complete.

Maintenance

ActivityActive
ResponsivenessNo issues