Observed since 20 Oct, 21:48 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
FastMCP server that delegates work from Claude Code to OpenAI Codex CLI and Google Gemini CLI as headless subprocesses.
MCP server that lets Claude Code delegate work to OpenAI Codex CLI and
Google Gemini CLI, running headlessly as subprocesses under your existing
ChatGPT / Google subscriptions (not pay-per-token API keys, except Gemini
which currently requires a GEMINI_API_KEY).
delegate_to_codex(prompt, cwd=".", timeout_s=270, sandbox="read-only", output_schema=None, task_category=None, max_prompt_chars=None, cache_ttl_s=None, recommendation_id=None, max_transient_retries=1, session_id=None)
Runs codex exec. Best for parallelizable, mechanical work: tests,
mechanical refactors, second-opinion code review. Pass output_schema (a
JSON Schema dict) to get a "structured" field back with Codex's answer
parsed as JSON, instead of scraping prose. Pass task_category (one of
"mechanical", "review", "large_context_analysis", "other" — see
recommend_route below) to tag this call in the ledger. The prompt is
always safe-tier compressed (see "Prompt compression" below); read-only
calls are also cached by default (see "Delegate result caching" below).
Transient backend errors (not rate limits, not timeouts) are retried
automatically within the call's own timeout budget (see "Retry on
transient errors" below); the result's "retry_count" reports how many
retries were actually used. Pass a prior result's "session_id" to
continue that conversation instead of resending context from scratch (see
"Session resume" below).delegate_to_gemini(prompt, cwd=".", timeout_s=270, approval_mode="plan", task_category=None, max_prompt_chars=None, cache_ttl_s=None, recommendation_id=None, max_transient_retries=1)
Runs gemini -p. Best for huge-context jobs: whole-codebase analysis,
long logs, large docs. Same compression/caching/retry behavior as
delegate_to_codex, scoped to approval_mode="plan" calls for caching.start_codex_job(...) / start_gemini_job(...) — same args as the two
above (plus a much longer default timeout_s, 1800s), but return
immediately with {"job_id", "status": "running"} instead of blocking.
Use these instead of the synchronous tools when a task could plausibly
run longer than ~4 minutes, since delegate_to_* is bounded by the MCP
client's own call timeout (see "Important" below), not just this
server's internal one.check_job(job_id) — poll a background job. Returns
{"status": "running", "tool", "elapsed_s"} while in flight, or
{"status": "done"|"error"|"cancelled", "tool", ...delegate result fields}
once finished. Jobs are kept in memory only (lost on server restart) and
pruned 1h after completion.cancel_job(job_id) — cancel a running background job instead of waiting
out its full timeout (up to 1800s). Kills the underlying subprocess tree
the same way a timeout does. Cancellation is requested, not instantaneous
— this call returns immediately with {"status": "cancelling"}; poll
check_job afterward for the terminal {"status": "cancelled", ...}.
Returns {"status": "not_found"} for an unknown job_id, or
{"status": <done|error|cancelled>, "already_finished": true} if the job
had already finished before the cancel request arrived.list_recent_sessions(tool="codex", limit=10) — list resumable Codex
conversations, most recently used first, as
{"session_id", "last_used_ago_s", "calls", "ever_resumed", "task_category"}. Lets you pick a session to resume via
delegate_to_codex's session_id param without having had to keep it
around from the original result. Codex only — tool="gemini" always
returns [] (a real answer, not an error; see "Session resume" below for
why Gemini has no resumable sessions at all).check_budgets() — local best-effort read of recent rate-limit hits per
platform (neither CLI exposes a real quota-remaining API), plus real
tokens_used_in_window for codex (parsed from its own "tokens used"
summary line; always 0 for gemini, which doesn't print an equivalent).get_delegation_log(limit=20, tool=None) — recent call history from the
SQLite ledger (ledger.sqlite3), so Claude doesn't blindly resend work
that already failed.recommend_route(task_category) — advisory routing recommendation
("codex", "gemini", or "claude") for one of the four fixed
categories: "mechanical", "review", "large_context_analysis",
"other". Falls back to a static default policy (mirroring the project's
original hand-written heuristic) until the leading platform's 95%
Wilson score confidence interval
for success rate stops overlapping the other platform's (see "Routing
confidence" below), then prefers whichever platform has the statistically
confident edge for that category. Ranks on success_rate only — Codex's
tokens_used and Gemini's duration_s aren't a comparable unit, so
they're reported separately per platform, not blended into one score.
"other" always recommends "claude" (there's no ledger baseline for
Claude doing a task itself). This tool never delegates anything itself —
it's advisory, and success_rate has no signal on whether an answer was
actually good, only that the subprocess exited cleanly. Pass
task_category and the returned recommendation_id through to whichever
delegate_to_*/start_*_job call follows, so the ledger keeps
accumulating comparable data and adherence_stats can report whether the
recommendation was actually followed.adherence_stats(task_category=None) — how often a delegate call's
platform actually matched recommend_route's recommendation for that
call. Descriptive only, not fed back into recommend_route's own ranking.
Only calls that actually supplied a recommendation_id count toward the
denominator; calls that never consulted recommend_route are excluded,
not counted as non-adherence.usage_report(days=7, task_category=None) — human-readable summary of
recent usage: per-platform success rate, avg duration, total/avg tokens
used, cache hit rate, rate-limit/transient-error counts, plus adherence
stats for the same window, plus (when not filtered to one category) a
breakdown of call volume per category. The raw ledger otherwise only
exists as individual rows you'd have to query by hand.Both delegates default to read-only / plan mode — they can inspect the target directory but not write files. Claude applies any resulting changes itself as diffs, rather than letting multiple agents write to the same tree concurrently.
Every prompt is run through a safe tier before sending: non-lossy whitespace normalization (collapsing blank-line runs, stripping trailing whitespace) that skips content inside triple-backtick fenced code blocks, since blank lines and trailing whitespace can be meaningful there (diff context, whitespace-sensitive fixtures) even though they never are in prose. This always runs; there's no way to disable it because there's no plausible input where it changes task meaning.
A lossy tier (hard truncation) is opt-in only, via max_prompt_chars.
There's no default cap — only opt in when you know truncation is safe for
that specific prompt, since cutting Claude-authored instructions can
silently change what a delegate is asked to do.
sandbox="read-only" (codex) / approval_mode="plan" (gemini) calls —
today's defaults — are cached for 15 minutes by default, keyed by an exact
hash of (tool, prompt actually sent, resolved cwd, mode, output_schema).
An identical repeat call returns the cached result ("cached": true,
"cache_age_s") instead of re-running. Any other mode never reads or writes
the cache — a write-capable call's side effects must never be silently
skipped by a cache hit. Only a fully successful result is ever cached (not
rate-limited, not a transient error, not timed out). Pass cache_ttl_s=0 to
force a fresh run for one call while still refreshing the cache for later
callers. A cache hit reuses a previously-drawn sample; it does not
re-verify the answer live, since Codex/Gemini are non-deterministic.
A transient backend error (e.g. Gemini's "high demand" 503) is retried
automatically, up to max_transient_retries times (default 1). Retries
happen within the call's own timeout_s budget, never additive to it —
this matters because delegate_to_* is bounded by the MCP client's own
call timeout, which uncapped retries could blow straight through. A retry
is skipped if less than ~20s of budget remains, or if the outcome is a real
rate limit (retrying immediately won't clear one) or a timeout (which
already consumed the full attempt budget). The result's "retry_count"
field reports how many retries were actually used; only the final outcome
gets a ledger row (intermediate failed attempts within one logical call
aren't separately recorded). Set max_transient_retries=0 to disable.
Every delegate_to_codex/start_codex_job result includes a "session_id"
(extracted from Codex's own startup banner, which prints session id: <UUID> on every call, fresh or resumed). Pass that id back in as
session_id on a later call to continue the same Codex conversation instead
of resending full context — this is a direct token saving, the whole point
of this project.
Codex only. Gemini's CLI has no stable per-session id, only --resume latest/--resume <index> — a fragile positional handle that any concurrent
Gemini session on the same machine can shift out from under you, silently
continuing the wrong conversation. There's no session_id param on
delegate_to_gemini/start_gemini_job.
A resumed call (session_id given):
cwd/sandbox — codex exec resume <id> is a distinct
subcommand that doesn't accept -C/-s/--color; it continues wherever
the original session was configured. Check the result's
"cwd_sandbox_ignored" field (true on resume, false on a fresh call).sandbox="read-only" — a resumed call's
meaning depends on prior turns a cache key can't represent.session_id is safe (unlike Gemini's per-attempt session rotation,
Codex reuses one session_id across all retries of a call).recommend_route decides when to trust the ledger's measured success rates
over the static default using a 95% Wilson score confidence
interval
per platform, not a flat sample-count cutoff or a flat percentage-point gap.
A platform is only recommended as "data-driven" when its interval's lower
bound exceeds every other candidate's interval's upper bound -- i.e. the two
platforms' plausible-success-rate ranges don't overlap at all.
This self-regulates with sample size in a way a flat threshold can't: a noisy 4-out-of-5 split produces a wide interval that (correctly) almost never separates from anything, while a real but modest edge backed by hundreds of samples can be trusted even though its raw percentage-point gap is much smaller than a flat tie-epsilon would have allowed through.
One deliberate tradeoff: comparing two individual platforms' confidence intervals for non-overlap is a well-known more conservative standard than a proper two-sample statistical test -- it demands more separation than a two-sample test would require at the same nominal 95% confidence. That's an intentional bias toward not declaring a winner on ambiguous data, consistent with this project's existing "don't trust a noisy small-sample leader" philosophy from before this change.
Install with pipx (recommended for end users) --
this puts the agent-dispatcher-mcp console script on PATH in its own
isolated environment, which is what lets claude mcp add invoke it by bare
name with no absolute paths:
pipx install .
(Once published, this becomes pipx install agent-dispatcher-mcp.)
Auth (one-time, done by you, not by Claude):
codex login (uses your ChatGPT account)GEMINI_API_KEY as a persistent user environment variable
(Google login is also supported, but this project currently assumes an
API key)Register with Claude Code:
claude mcp add dispatcher -- agent-dispatcher-mcp
Important: also set a per-server timeout (milliseconds) in the resulting
.mcp.json entry. Claude Code's own MCP client can cut a tool call off well
before this server's internal timeout_s (default 270s) elapses, and
codex exec/gemini -p against a real repo routinely take 60-90s+, so the
client-side default is too tight. Example entry:
{
"mcpServers": {
"dispatcher": {
"command": "agent-dispatcher-mcp",
"args": [],
"timeout": 300000
}
}
}
Restart the Claude Code session after editing .mcp.json -- server configs
(including timeout) are only read at session start.
The SQLite ledger/cache live in a per-user data directory (via
platformdirs --
%LOCALAPPDATA%\agent-dispatcher-mcp\ledger.sqlite3 on Windows,
~/.local/share/agent-dispatcher-mcp/ledger.sqlite3 on Linux,
~/Library/Application Support/agent-dispatcher-mcp/ledger.sqlite3 on
macOS), not next to the installed package. Override with the
AGENT_DISPATCHER_DATA_DIR environment variable if needed.
python -m venv .venv
.venv/Scripts/pip install -e ".[dev]" # Windows; editable install + pytest
An editable install (pip install -e .) does not put the console script
on PATH the way pipx install does -- for pointing a dev's own Claude Code
session at uncommitted changes, register with the venv python directly
instead: claude mcp add dispatcher -- "<repo>\.venv\Scripts\python.exe" -m agent_dispatcher_mcp.server.
.venv/Scripts/python.exe -m pytest tests/
Covers the pure logic only (executable resolution, rate-limit vs transient-error classification, ledger/cache read-writes, prompt compression, caching, and adherence tracking) -- no real codex/gemini subprocess calls, so it's fast and doesn't burn API/subscription quota.
.venv/Scripts/python.exe tests/manual_check.py
Publishing is automated via .github/workflows/publish.yml, triggered by
pushing a v*.*.* tag. It runs the test suite, builds the sdist/wheel,
twine checks them, and uploads to PyPI using
trusted publishing (OIDC) --
no PyPI API token is ever generated or stored as a GitHub secret.
One-time setup on PyPI (only you can do this -- requires login):
agent-dispatcher-mcp hasn't been published yet, register a
"pending publisher"
for it at https://pypi.org/manage/account/publishing/ with:
agent-dispatcher-mcpmaherphineagent-dispatcher-mcppublish.ymlpypi
This works even though the GitHub repo is private -- trusted publishing
checks the OIDC claims from the workflow run (repo/owner/workflow/
environment), not repo visibility.pypi and add required reviewers -- this adds a manual
approval gate before any publish job can run, independent of PyPI's own
trusted-publisher check.To cut a release (after the above is done once):
git tag v0.1.0
git push origin v0.1.0
Then watch it with gh run watch like any other workflow. Bump the
version in pyproject.toml before tagging a new release -- PyPI rejects
re-uploading an existing version number.
The pure-logic test suite deliberately never shells out to real codex/
gemini, which is great for speed but has a real blind spot: it can't
catch bugs in how this server interprets the real CLIs' actual output.
Both of these were found by live testing, not the test suite, and are worth
knowing about since they silently distorted real usage data for a while:
_record_and_summarize used to scan a call's entire output for
error patterns regardless of whether it actually succeeded. If a
delegate's own correct answer happened to discuss rate limiting or HTTP
status codes as subject matter (e.g. asked to summarize this very
codebase, or any code that handles API errors), its accurate answer
tripped the classifier -- a false rate_limited=True reading then marked
the platform unavailable in check_budgets() for its full assumed window
(up to 24h for Gemini), suppressing real, working usage without any
actual error occurring. Fixed: classification now only runs on non-success
outcomes -- a call that exited 0 cleanly cannot coherently be "currently
rate limited."tokens_used extraction only ever scanned stdout, so it silently
stayed None for every real Codex call. Confirmed via direct testing:
stdout is just the raw answer text; the whole banner/progress/summary
output (including "tokens used") goes to stderr. Fixed: extraction now
checks both streams.codex exec resume rejects --color. Unlike the fresh codex exec
path, the resume subcommand errors with unexpected argument '--color' found if it's passed -- caught during pre-flight live testing before the
session-resume feature was wired up, not guessed at from --help output
alone. Session resume's real conversational continuity (not just "the
subprocess exited 0") was also verified live -- tests/manual_check.py
asks Codex to remember a number in one call, then resumes with
session_id and confirms the follow-up actually recalls it -- since the
pure-logic suite's mocked fixtures can't prove real cross-call state
persists on the CLI's side.cancel_job's subprocess-tree kill was verified live, not just
mocked. The pure-logic tests for cancel_job mock _do_codex entirely,
which proves the job-registry plumbing (the asyncio.Task gets cancelled,
check_job reports "cancelled") but can't prove the real subprocess
actually dies rather than being orphaned -- the exact class of bug the
original timeout-handling code in this same function had to solve.
Verified live: started a real codex exec job, cancelled it mid-flight,
and confirmed via tasklist that its PID was actually gone immediately
after _kill_process_tree ran.codex/gemini install as
.CMD shims. Plain asyncio.create_subprocess_exec("codex", ...) fails
with FileNotFoundError because Win32 CreateProcess only auto-appends
.exe, not .cmd. Fixed by resolving the executable via shutil.which()
first (_resolve_executable in server.py).node/codex process on Windows. Killing only the
immediate child leaves the grandchild alive holding the stdout/stderr
pipes open, so a naive "kill then read remaining output" hangs forever.
Fixed with taskkill /PID <pid> /T /F (_kill_process_tree), plus a
bounded secondary read so a delegate call can never hang the caller
indefinitely, no matter what.EMFILE: too many open files. Passing an explicit --session-id (a fresh UUID) avoids
triggering that replay — already wired into the default flags used here.503 UNAVAILABLE ("high demand") errors independent of the above — that's on
Google's side, not this server; check_budgets/the ledger will surface
repeated failures but can't distinguish rate-limiting from a backend
outage beyond pattern-matching the error text.MIT — see LICENSE.
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.2.0 | 11 Aug 2026 | 5 |
This describes our collected data; it is not a security assessment.
Observed since 20 Oct, 21:48 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:105f016c5c40aa5bf362bae1af041bd2cd2458e5816c2af012b8e72aa616c894