
parithosh-varma/taskbridge
Analytics & monitoringLet Claude dispatch coding work to headless OpenCode agents via MCP — durable SQLite queue, auto-worker, two-way questions, worktree isolation
Overview
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. 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: - — breaks work down, delegates, and reads results. - — edits, tests, shell, headlessly and reproducibly. - — survives restarts, supports retries, priorities, dependencies, and two-way questions.
README
TaskBridge
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
- 16 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,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/.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 via ngrok (the installer prompts for your Authtoken) or Cloudflare quick tunnel with automaticMCP_ALLOWED_HOSTSpairing. - 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. 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, opencode CLI on PATH, and (for public access) a free ngrok account.
One command — install, tunnel, serve, print your connector box:
git clone https://github.com/Parithosh-Varma/TaskBridge.git
cd TaskBridge
./install.sh
install.sh prompts for your ngrok Authtoken (blank = already configured), installs dependencies into .venv, generates or reuses the bearer token in .bridge-token, opens the ngrok tunnel (fixed domain from .ngrok-domain when present), pairs MCP_ALLOWED_HOSTS, prints the public URL + token box, then 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 `` 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
/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) — see worker.py header and adjust the plist ProgramArguments to your checkout path.
Usage
From Claude:
submit_task(title="Add retry tests", instructions="Steps..., paths..., acceptance criteria...", working_dir="/path/to/target-repo")→ returnsidwait_for(task_ids=[""], timeout_sec=120)— blocks instead of pollingget_task_result(task_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 liveness
From OpenCode (manual worker loop):
./.venv/bin/python tasks_cli.py pending
./.venv/bin/python tasks_cli.py show
./.venv/bin/python tasks_cli.py claim
# ... do the work (edits, shell, tests) ...
./.venv/bin/python tasks_cli.py complete --result "..."
./.venv/bin/python tasks_cli.py fail --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
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 |
worker.py |
Headless executor: opencode run --auto, timeouts, reaper, worktrees, banners |
claude_control_mcp.py |
Reverse MCP server (OpenCode → Claude) |
install.sh |
One command: deps + bearer token + ngrok tunnel + allowlist + serve |
start_public.sh |
Background variant: tunnel + allowlisted MCP server (ngrok fixed domain or Cloudflare) |
claude_desktop_config.snippet.json |
Claude Desktop wiring template (`` placeholder) |
Configuration
| Variable | Used by | Purpose |
|---|---|---|
TASK_DB_PATH |
bridge, CLI | SQLite path (default: /tasks.db) |
WORKER_MODEL |
worker | Default model, e.g. opencode/cat-jam-free |
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 |
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 prompts for the ngrok Authtoken and serves MCP in the foreground.
Project structure
.
├── opencode_bridge_mcp.py # MCP server (Claude → queue)
├── task_store.py # SQLite queue logic
├── tasks_cli.py # Manual worker CLI
├── worker.py # Auto-worker (launchd, 60s)
├── 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)
├── claude_desktop_config.snippet.json
├── claude-clean.settings.json
├── requirements.txt # mcp>=1.0,<2
└── .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.
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/.log. Stalerunningtasks are reaped after 15 min (per-tasktimeout_sec+ slack honored). - Double-run after kill: every attempt spawns
opencode run --standalone --title br--a(per-attempt argv marker, ps-visible). 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. - Long tasks (up to 1800s
timeout_sec) are supported, but the 60s launchd cadence means pickup latency averages ~30s.
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: OK
License
Apache-2.0 — see LICENSE. Fork it, self-host it, build on it.
Install
This server does not publish a one-line install command.
Open the repository installation guide