Skip to main content

agent-dispatcher-mcp

CI

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).

Tools

  • 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.

Prompt compression

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.

Delegate result caching

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.

Retry on transient errors

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.

Session resume

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):

  • Ignores cwd/sandboxcodex 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).
  • Is never cached, even with sandbox="read-only" — a resumed call's meaning depends on prior turns a cache key can't represent.
  • Retries the same way a fresh call does: a transient blip during a resume attempt means nothing in the session was mutated, so retrying with the same session_id is safe (unlike Gemini's per-attempt session rotation, Codex reuses one session_id across all retries of a call).

Routing confidence (Wilson score intervals)

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.

Setup

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: codex login (uses your ChatGPT account)
  • Gemini: set 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.

Development

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.

Tests

.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.

Manual verification (real CLIs, slower)

.venv/Scripts/python.exe tests/manual_check.py

Releasing (publishing to PyPI)

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):

  1. Create a PyPI account at https://pypi.org/account/register/ if you don't have one, with 2FA enabled (PyPI requires it).
  2. Since agent-dispatcher-mcp hasn't been published yet, register a "pending publisher" for it at https://pypi.org/manage/account/publishing/ with:
    • PyPI Project Name: agent-dispatcher-mcp
    • Owner: maherphine
    • Repository name: agent-dispatcher-mcp
    • Workflow name: publish.yml
    • Environment name: pypi 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.
  3. Optionally, in the GitHub repo's Settings -> Environments, create an environment named 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.

Notable bugs found via live testing (fixed)

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:

  • A successful call could get misclassified as rate-limited/a transient error. _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."
  • Codex's "tokens used" summary line is printed to stderr, not stdout. 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.

Known quirks (Windows)

  • npm CLI shims need PATHEXT resolution. 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).
  • Timeouts must kill the whole process tree. Both CLIs re-exec themselves as a child 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.
  • Gemini's first run per working directory can be very slow due to an upstream bug where it tries to replay/reload a huge range of historical (mostly nonexistent) session files, spamming 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.
  • Gemini's API has also been observed returning transient 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.

License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agent_dispatcher_mcp-0.2.0.tar.gz (54.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agent_dispatcher_mcp-0.2.0-py3-none-any.whl (39.2 kB view details)

Uploaded Python 3

File details

Details for the file agent_dispatcher_mcp-0.2.0.tar.gz.

File metadata

  • Download URL: agent_dispatcher_mcp-0.2.0.tar.gz
  • Upload date:
  • Size: 54.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for agent_dispatcher_mcp-0.2.0.tar.gz
Algorithm Hash digest
SHA256 c4c2865c4906c3d0097553bccfa4ac34b7304fedc2528f4ba57c643b24e55b8e
MD5 1bcbefc991aba2f3916916164b3b3178
BLAKE2b-256 d42a17631df071a11347afe37ec816270a1169ee3f00ef114f5f555cefe8f446

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_dispatcher_mcp-0.2.0.tar.gz:

Publisher: publish.yml on maherphine/agent-dispatcher-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file agent_dispatcher_mcp-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for agent_dispatcher_mcp-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8aa973fa9d55b0718ce7d1c88c65ccd1a8e8cb179bda9b2045718f18a4530aa3
MD5 3fa61fa82a2d706818294275aa55bbff
BLAKE2b-256 d00c492f33b2f11979fc6bbd7c2408e7ff39e8812272a4136968c4a211a7c3f7

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_dispatcher_mcp-0.2.0-py3-none-any.whl:

Publisher: publish.yml on maherphine/agent-dispatcher-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page