TaskBridge MCP server
Commit Oct 19, 2026Enables Claude to dispatch coding tasks to headless OpenCode agents via a durable SQLite queue with worktree isolation and two-way communication.
Also known as Parithosh-Varma/TaskBridge
- Stars
- 3
- Tools
- —
- Downloads, 30 days
- —
- Latest version
- —
- Last commit
- Oct 19, 2026
- License
- Apache-2.0
What is TaskBridge MCP?
TaskBridge enables Claude to dispatch coding tasks to headless OpenCode agents via a durable SQLite queue with worktree isolation and two-way communication.
Summary generated from the project's README
About TaskBridge MCP server
Developers can delegate complex coding tasks from Claude to headless OpenCode agents using TaskBridge. This system uses a durable SQLite queue to manage the handoff between Claude, which acts as the high-level planner, and OpenCode, which handles the actual filesystem edits, testing, and shell execution. The setup supports local Claude Desktop via stdio or remote clients over HTTP, with options to expose the server through Cloudflare quick tunnels or ngrok.
The server provides 17 specialized tools including submit_task, submit_batch, and wait_for to manage the execution lifecycle. It features worktree isolation to ensure each task runs in a separate git branch and includes a two-way communication system where workers can pause to ask questions that Claude answers via answer_task. For production reliability, it includes a worker supervisor that can recover stale processes using the restart_worker command and a live dashboard for monitoring task logs and queue status.
What you can do with it
- 1Batching multiple coding tasksA developer can send a group of up to 20 related coding tasks to the agent at once.
- 2Handling interactive coding questionsWhen an agent hits a roadblock, it can pause and wait for the user to provide more information.
- 3Recovering from failed worker processesIf a background worker becomes unresponsive, the system can trigger a restart through the MCP server.
Written from the project's README. Every use case is checked against a quote from it.
README
View on GitHubTaskBridge
Claude plans. OpenCode builds. Dispatch real coding work from Claude to headless OpenCode agents via a durable SQLite queue.
Claude submits tasks over MCP. An auto-worker picks them up in ~60 s, runs opencode run in an isolated worktree, and returns a structured summary, git-aware diff, logs, and usage for Claude to read. Works from Claude Desktop (stdio), any MCP client over HTTP, and in reverse — OpenCode can drive Claude too.
Why this exists
Claude is a powerful planner that can't touch your filesystem. OpenCode runs code locally but lives outside Claude's context. The bridge closes the loop:
- Claude stays the interface — breaks work down, delegates, and reads results.
- OpenCode does the execution — edits, tests, shell, headlessly and reproducibly.
- SQLite is the durable handoff — survives restarts, supports retries, priorities, dependencies, and two-way questions.
Features
- 17 MCP tools for Claude —
submit_task,submit_batch(up to 20),get_task/get_task_result,get_task_logs(live tail),wait_for,answer_task,list_tasks,queue_status/worker_heartbeat,restart_worker(recover a dead/stale worker through launchd, from the MCP server itself),add_task_note,cancel_task/clear_finished,list_models/set_default_model,ping. No polling loops, full observability. - Headless auto-worker (
worker.py) — launchd polls every 60 s, claims oldest-first by priority, streams output totask_logs/<id>.log, returns structured{summary, files_touched, tests_run, open_questions}plus a git-aware diff. - Two-way questions — worker can pause with a
questionblock → task goeswaiting+ macOS banner → Claude answers viaanswer_task→ run resumes with the answer in context. - Worktree isolation — per-task branch +
git worktree, auto-committed on success, cleaned up unlessworktree_keep. - Honest permissions —
readonlyreverts writes and reportsrevertedfiles;no-network/no-shellare prompt-level and explicitly labeled. - Reverse bridge (
claude_control_mcp.py) — 6 tools for OpenCode to drive Claude:claude_ask(sync),claude_submit/claude_poll/claude_result/claude_cancel(background),claude_status. Uses Anthropic API ifANTHROPIC_API_KEYis set, elseclaude -p. - Public-URL mode (
install.sh/start_public.sh) — one command to expose MCP through a temporary Cloudflare quick tunnel (default, no account needed) or ngrok, with automaticMCP_ALLOWED_HOSTSpairing. The installer asks which tunnel to use. - Live dispatch board (
bridge_dashboard.html) — served atGET /on the same host. Tally counts, per-task live logs, reverse jobs, engine tail, and a scrolling feed of fresh output. The Model button sets the worker default model (persisted toworker_config.json) without touching the CLI. Polls/api/*; MCP stays at/mcp. - Caller detection —
submit_task/submit_batchstamp the true caller from the MCP handshakeclientInfo, so Claude and ChatGPT dockets stay separate.origincan be overridden explicitly. - Production reliability — per-attempt timeouts, 15-min stale-
runningreaper with pid-reuse guard, single-instance flock, heartbeat file, macOS banners (WORKER_NOTIFY=0to silence).
Quickstart
Prerequisites: Python 3.12 and opencode CLI on PATH. Public access needs no account by default — the installer offers a temporary Cloudflare quick tunnel; a free ngrok account is only needed if you pick ngrok (reserved domain / existing Authtoken).
One command — install, tunnel, serve, print your connector box:
git clone https://github.com/Parithosh-Varma/TaskBridge.git
cd TaskBridge
./install.sh
install.sh first asks which tunnel to use — Cloudflare quick tunnel (default, no account, random trycloudflare.com URL) or ngrok (prompts for your Authtoken; fixed domain from .ngrok-domain when present). Skip the prompt with --tunnel cloudflare / --tunnel ngrok. It then installs dependencies into .venv, generates or reuses the bearer token in .bridge-token, opens the tunnel, pairs MCP_ALLOWED_HOSTS, prints the public URL + token box, and serves MCP in the foreground. Ctrl+C stops the server; the tunnel stays up for the next run. Rotate the token with ./install.sh --rotate.
Manual setup (stdio / local dev):
git clone https://github.com/Parithosh-Varma/TaskBridge.git
cd TaskBridge
python3.12 -m venv .venv
./.venv/bin/pip install -r requirements.txt
Run the MCP server (stdio — Claude Desktop spawns this):
./.venv/bin/python opencode_bridge_mcp.py
Or serve HTTP locally:
./.venv/bin/python opencode_bridge_mcp.py --transport streamable-http --port 8000
# → http://127.0.0.1:8000/mcp
Wire into Claude Desktop — merge claude_desktop_config.snippet.json into
~/Library/Application Support/Claude/claude_desktop_config.json under mcpServers
(replace <REPO_PATH> with your checkout path), then restart Claude Desktop.
Verify: ask Claude to list MCP tools — you should see submit_task, get_task, list_tasks under taskbridge.
Authentication (HTTP mode)
install.sh generates the bearer token and serves the public endpoint:
./install.sh # reuse or generate .bridge-token, tunnel + serve
./install.sh --rotate # regenerate the bearer token first
Token is loaded from .bridge-token file or BRIDGE_AUTH_TOKEN env var.
install.sh and start_public.sh (the background / Cloudflare-tunnel variant) both pick it up automatically.
Public URL access requires:
Authorization: Bearer <your-token>
/healthis open (monitoring)/dashboard prompts for token (stored in sessionStorage)/api/*and/mcprequire Bearer token- stdio transport (Claude Desktop) is local-only, no token needed
To rotate token: ./install.sh --rotate
Run one worker pass manually:
./.venv/bin/python worker.py
Or schedule it every 60s with launchd (macOS):
./launchd/install_worker_agent.sh # install/update the user LaunchAgent, then start
That installs ~/Library/LaunchAgents/com.taskbridge.worker.plist (a user agent —
no sudo) from launchd/com.taskbridge.worker.plist.template, with paths for this
checkout. See launchd/README.md for label verification,
update/uninstall, and the restart_worker contract.
Usage
From Claude:
submit_task(title="Add retry tests", instructions="Steps..., paths..., acceptance criteria...", working_dir="/path/to/target-repo")→ returnsidwait_for(task_ids=["<id>"], timeout_sec=120)— blocks instead of pollingget_task_result(task_id="<id>")→result,structured,usage,diff
Batch + follow-ups:
submit_batch(tasks=[...])— up to 20 tasks with per-itemmodel,depends_on,priority,timeout_sec,max_retries/retry_model,tags,idempotency_key,permission,worktree_*get_task_logs(task_id="...", since_offset=0)— tail live outputanswer_task(task_id="...", answer="...")— answer awaitingtask's questionqueue_status()/worker_heartbeat()— queue counts + daemon livenessrestart_worker(force=false, timeout_sec=90)— recover the worker through launchd when it is offline/stale (orforce=trueto restart a healthy one deliberately); returns a verified result withprevious_status,restart_method, heartbeat before/after andnext_stepson failurelist_models(search="")/set_default_model(model="opencode/deepseek-v4.1-flash-free")— change the worker-wide default model (validated againstopencode models, saved toworker_config.json). The dashboard's Model button is the same picker: it callsGET /api/models+POST /api/modelbehind the bearer token.
From OpenCode (manual worker loop):
./.venv/bin/python tasks_cli.py pending
./.venv/bin/python tasks_cli.py show <id>
./.venv/bin/python tasks_cli.py claim <id>
# ... do the work (edits, shell, tests) ...
./.venv/bin/python tasks_cli.py complete <id> --result "..."
./.venv/bin/python tasks_cli.py fail <id> --error "..."
Reverse (OpenCode → Claude) via claude_control_mcp.py (registered as an OpenCode MCP server): claude_ask for one-shot answers, claude_submit + claude_poll / claude_result for background jobs.
How it works
flowchart LR
Claude -- MCP stdio/HTTP --> Bridge[opencode_bridge_mcp.py]
Bridge -- read/write --> DB[(tasks.db - SQLite)]
CLI[tasks_cli.py] -- claim/complete --> DB
Worker[worker.py - launchd 60s] -- polls DB, runs opencode run --> Target[working_dir / worktree]
Worker -- streams logs --> Logs[task_logs/ID.log]
Worker -- result/structured/usage --> DB
Claude -- get_task_result / wait_for --> Bridge
Bridge -- restart_worker --> Supervisor[worker_supervisor.py] -- launchctl --> Worker
OpenCode -- claude_ask/submit --> Reverse[claude_control_mcp.py]
Reverse --> ClaudeCLI[claude -p / Anthropic API]
Key components:
| File | Role |
|---|---|
opencode_bridge_mcp.py |
MCP server (Claude side), stdio + streamable-http / sse, host allowlist |
task_store.py |
Shared SQLite logic: submit/claim/complete, retries, deps, notes, heartbeat |
tasks_cli.py |
CLI for manual runs: pending list show claim complete fail logs heartbeat answer restart-worker supervisor-status |
worker.py |
Headless executor: opencode run --auto, timeouts, reaper, worktrees, banners |
worker_supervisor.py |
Independent restart engine behind restart_worker (launchd control, cooldown, verification); imports nothing from worker.py |
claude_control_mcp.py |
Reverse MCP server (OpenCode → Claude) |
install.sh |
One command: deps + bearer token + tunnel choice (Cloudflare quick tunnel or ngrok) + allowlist + serve |
start_public.sh |
Background variant: tunnel + allowlisted MCP server (ngrok fixed domain or Cloudflare) |
launchd/ |
Worker LaunchAgent template + install/uninstall script + supervision docs |
claude_desktop_config.snippet.json |
Claude Desktop wiring template (<REPO_PATH> placeholder) |
Configuration
| Variable | Used by | Purpose |
|---|---|---|
TASK_DB_PATH |
bridge, CLI | SQLite path (default: <repo>/tasks.db) |
WORKER_MODEL |
worker | Default model fallback, e.g. opencode/deepseek-v4.1-flash-free (worker_config.json from the dashboard / set_default_model wins over it) |
OPENCODE_BIN |
worker | opencode binary (default: opencode on PATH) |
WORKER_NOTIFY |
worker | 1 (default) macOS banners, 0 to silence |
MCP_ALLOWED_HOSTS |
bridge (HTTP) | Comma-separated tunnel hosts (required behind ngrok/cloudflared, else 421) |
BRIDGE_AUTH_TOKEN |
bridge (HTTP) | Bearer token for MCP/API access (defaults to .bridge-token file) |
TASKBRIDGE_CONSENT_SECRET |
OAuth consent | Secret for the /approve "this was me" page (defaults to the bridge token) |
TASKBRIDGE_TOKEN_TTL_SEC |
OAuth | Access-token lifetime (default 86400 = 24h) |
TASKBRIDGE_REFRESH_TTL_SEC |
OAuth | Refresh-token lifetime (default 2592000 = 30d) |
MCP_PUBLIC_URL |
bridge (HTTP) | Force the public origin in OAuth documents when behind unusual proxies |
ANTHROPIC_API_KEY |
reverse bridge | Direct API backend; else claude -p CLI |
CLAUDE_API_MODEL / CLAUDE_MCP_HOME |
reverse bridge | Model override / shadow HOME for CLI backend |
TASKBRIDGE_SERVICE_LABEL |
restart_worker |
LaunchAgent label (default com.taskbridge.worker) |
TASKBRIDGE_RESTART_COOLDOWN_SEC |
restart_worker |
Minimum gap between restart attempts (default 90) |
TASKBRIDGE_RESTART_TIMEOUT_SEC |
restart_worker |
Default verified-recovery timeout (default 90) |
TASKBRIDGE_RESTART_GRACE_SEC |
restart_worker |
Max wait for a running pass before kickstart -k (default 20) |
start_public.sh: uses .ngrok-domain if present (fixed domain), else Cloudflare quick tunnel. It re-pairs MCP_ALLOWED_HOSTS automatically — rerun it whenever the URL changes. If .bridge-token exists, BRIDGE_AUTH_TOKEN is loaded automatically. install.sh is the interactive equivalent — it asks which tunnel to use (Cloudflare quick tunnel by default, ngrok optional), installs cloudflared/ngrok as needed, and serves MCP in the foreground. Set TASKBRIDGE_TUNNEL=cloudflare|ngrok to answer that prompt non-interactively.
Project structure
.
├── opencode_bridge_mcp.py # MCP server (Claude → queue)
├── task_store.py # SQLite queue logic
├── tasks_cli.py # Manual worker CLI (+ restart-worker)
├── worker.py # Auto-worker (launchd, 60s)
├── worker_supervisor.py # restart_worker engine: launchd control, cooldown, verify
├── claude_control_mcp.py # Reverse bridge (OpenCode → Claude)
├── install.sh # One-command installer: deps, token, tunnel, serve
├── start_public.sh # Public tunnel + server launcher (background)
├── launchd/ # Worker LaunchAgent template + install/uninstall docs
├── tests/ # pytest suite (supervisor, MCP tool surface, launchd E2E)
├── claude_desktop_config.snippet.json
├── claude-clean.settings.json
├── requirements.txt # mcp>=1.0,<2
├── requirements-dev.txt # pytest (tests only)
└── .gitignore # excludes .venv/, tasks.db, logs, task_logs/, claude_jobs/, worktrees/
Runtime state (never committed): tasks.db, task_logs/, claude_jobs/, worktrees/, *.log, heartbeat.json, .worker.lock, .restart_state.json.
Troubleshooting / FAQ
- 421 behind tunnel:
MCP_ALLOWED_HOSTSmust exactly match the tunnel host. Run./install.shor./start_public.sh— they set this automatically. - Tunnel URL changed (Cloudflare): quick-tunnel hostnames rotate every restart. Rerun
./install.sh(or./start_public.sh), or reserve an ngrok fixed domain via.ngrok-domain. - Worker looks stuck: check
./.venv/bin/python tasks_cli.py heartbeatandtask_logs/<id>.log. Stalerunningtasks are reaped after 15 min (per-tasktimeout_sec+ slack honored). - Worker offline / heartbeat stale: call MCP
restart_worker()— it restarts through launchd from the MCP server process (which does not depend on the worker), waits for a verified fresh heartbeat, and explains itself (outcome,next_steps). Same engine from a terminal:./.venv/bin/python tasks_cli.py restart-worker [--force]. Full runbook: launchd/README.md. restart_workersaysservice_missing: no LaunchAgent is installed for this checkout — run./launchd/install_worker_agent.sh, then retry.restart_workersaysrestart_timeout: launchctl started the job but no heartbeat appeared.launchctl print "gui/$(id -u)/com.taskbridge.worker" | grep -E 'state|last exit'—last exit code 78(EX_CONFIG) means launchd could not run the configured program (wrong paths in the plist, missing.venv); otherwise read the tail ofworker.launchd.log/worker.log.- Double-run after kill: every attempt spawns
opencode run [--standalone] --title br-<id>-a<n>(per-attempt argv marker, ps-visible). Optional flags such as--standaloneare probed from the binary's ownrun --help(-hfallback) once per install and only passed when advertised; if a build still rejects the command (help dump, nonzero exit), the worker retries once without them. Unreadable or older/newer variants just run without the extras — no version tables; the reaper then relies on the pid-tree/argv walk alone. The reaper kills the orphaned CLI child plus its ppid tree (loop-until-empty, hard-capped so a matching bug can't mass-kill); a per-pass sweep catches tagged strays of non-running tasks. Reaper deadline is per-task (max(900s, timeout_sec + 120s)). macOS gives no session/env signal (ps sess=is 0 everywhere,ps -Eshows no env), so ppid walk + argv marker are the whole toolkit — reparented-to-1/setsid grandchildren are the known residual. NOTE: never pass--sessionto the CLI: it makes free-tier models refuse to run. - No banners: macOS only. Set
WORKER_NOTIFY=0to silence intentionally; full output is always intasks.db+worker.log. - Reverse bridge fails fast (~1s): run
claude loginonce or setANTHROPIC_API_KEY. opencodenot found: put it onPATHor setOPENCODE_BIN=/path/to/opencode.
Limitations
- Token counts aren't exposed by the
opencodeCLI —usagereports model, latency, attempts, and sandbox status, not tokens. no-network/no-shellare prompt-level restrictions, not OS-enforced sandboxes (documented inworker.py).- Desktop banners and launchd scheduling are macOS-specific; Linux needs its own scheduler (systemd/cron) +
worker.pystill runs manually.restart_workeris likewise macOS/launchd-only — on a host without launchd it returnssupervisor_unavailablewith manual instructions instead of pretending. - Long tasks (up to 1800s
timeout_sec) are supported, but the 60s launchd cadence means pickup latency averages ~30s. restart_workercannot recover from a broken plist either: if the configured program can't run, the result isrestart_timeoutwith the launchd diagnostics you need. Queued tasks are untouched either way.
Verified
task_storesubmit → claim → complete round-trip: OK- MCP
submit_task/get_task/list_tasksviacall_tool: OK - CLI
pending/show/claim/complete: OK submit_task→ auto-picked-up in ~60s →get_task_resultwith zero manual steps: OKrestart_workerregistered + schema (forceboolean default false,timeout_secinteger) via MCP tool discovery: OKrestart_workerunit suite (skip/offline/stale/force, missing service, permission denied, launchctl failure, timeout, concurrency, cooldown, queue preservation, label discovery) with a faked launchctl: OKrestart_workeragainst real launchd with a disposable probe LaunchAgent: offline → fresh heartbeat, stale →kickstart -kreplaces the process, healthy +force=false→ skipped, pending task byte-identical after recovery: OK (TASKBRIDGE_RUN_LAUNCHD_E2E=1)
License
Apache-2.0 — see LICENSE. Fork it, self-host it, build on it.
TaskBridge MCP questions
How does TaskBridge handle task execution?
An auto-worker picks up tasks from a SQLite queue every 60 seconds and runs them in an isolated git worktree using the OpenCode CLI.
Can the worker ask Claude questions during a task?
Yes, the worker can pause a task with a question block, which triggers a macOS banner and allows Claude to respond via the answer_task tool.
How can I expose the MCP server to the internet?
You can use the install script to expose the server through a Cloudflare quick tunnel or ngrok.