claude-bridge
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@claude-bridge dispatch(prompt='review auth middleware', cwd='/workspace')"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
claude-bridge
An MCP server that lets host-side Claude Cowork dispatch work into
Claude Code running inside your devcontainer, over stdio. Cowork
launches the bridge with docker exec -i; the bridge shells to
claude -p and returns a structured result.
Install
Inside the devcontainer:
# from PyPI (once published — see CHANGELOG.md):
pip install claude-bridge-mcp
# or pin to the latest main branch:
pip install "git+https://github.com/JosiahSiegel/claude-bridge.git@main"
# or for development (editable + dev deps):
git clone https://github.com/JosiahSiegel/claude-bridge.git
cd claude-bridge
pip install -e ".[dev]"Note on names: the PyPI distribution is
claude-bridge-mcp(the bareclaude-bridgename was already taken by an unrelated HTTP gateway). The Python import (import claude_bridge), the CLI (claude-bridge), and the MCP server name (claude-bridge) are unchanged — only thepip installargument differs.
A reference Dockerfile and devcontainer.json for downstream projects
live in examples/devcontainer/.
Related MCP server: Clanker
Quickstart
Three things have to be true: the bridge installed in the container, its absolute path in the Claude Desktop config on the host, and Claude Desktop fully restarted.
1. Install in the container.
pip install claude-bridge-mcp
which claude-bridge # copy this path — you'll paste it in step 22. Register with Claude Desktop on the host.
Open via Settings → Developer → Edit Config, or edit the file directly:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:
%APPDATA%\Claude\claude_desktop_config.jsonLinux:
~/.config/Claude/claude_desktop_config.json
{
"mcpServers": {
"devcontainer-claude": {
"command": "docker",
"args": [
"exec", "-i",
"-e", "CLAUDE_BRIDGE_CWD=/path/to/clean/dir",
"<container-name-from-docker-ps>",
"/absolute/path/from/which/claude-bridge"
]
}
}
}The CLAUDE_BRIDGE_CWD line is important — see
Recommended pattern below for why.
3. Fully quit Claude Desktop (⌘Q / system-tray exit, not close
window) and relaunch. In a Cowork session, ask Claude to call
list_channels. {"channels": {}} means it works.
4. Dispatch.
For quick prompts (under ~60s round trip):
dispatch(prompt="say ok", channel="smoke")For real work (anything that might exceed the MCP transport's per-call ceiling — persona runs, refactors, work in a busy project):
job = dispatch_async(
prompt="audit the auth middleware",
channel="auth-audit",
cwd="/workspace", # the project you want claude to work in
timeout_seconds=900
)
# Then poll wait_dispatch(job["job_id"], max_wait_seconds=50) in a loop.For watch-this-condition-over-hours work, use schedule_dispatch —
each tick is its own short job, the bridge owns the cron loop, and
the prompt can self-cancel by emitting [BRIDGE_STOP_SCHEDULE].
schedule_dispatch(
prompt="gh pr list --state open. If all merged, end with [BRIDGE_STOP_SCHEDULE]. Otherwise summarize.",
channel="pr-watcher",
interval_seconds=300,
until_seconds=14400, # stop trying after 4 hours
permission_mode="bypassPermissions"
)See Tools → Polling pattern for the long-job loop and Recurring for schedules.
For agents using the bridge programmatically: call bridge_help()
first. It returns a structured guide to every tool, the four canonical
workflows, and the gotchas. Designed to be the single discoverability
entry point so agents don't have to skim the README.
Recommended pattern
The bridge runs claude -p in its own cwd by default. If that cwd is a
busy project workspace (with .claude/settings.local.json enabling
several MCP servers and plugins, plus a large CLAUDE.md), every
dispatch pays a 30s+ cold-start cost — sometimes long enough to time
out.
The fix is to anchor the bridge in a clean directory and retarget individual dispatches at the project they actually want:
Set
CLAUDE_BRIDGE_CWDin the MCP config to a clean dir (e.g. the bridge's own checkout, or any directory without.claude/).Pass
cwd="/workspace"(or whatever) per dispatch when you need project context.Bump
timeout_secondsfor those calls — first invocation in a project has to load all its MCP servers.
list_channels and a bare dispatch(prompt="say ok") then stay snappy;
heavy project work gets routed to the right cwd on demand.
Running unattended (max permissions)
For autonomous operation — letting Cowork drive the bridge without
permission prompts blocking dispatches — set the bridge's default
permission mode to bypassPermissions in the MCP config:
{
"mcpServers": {
"devcontainer-claude": {
"command": "docker",
"args": [
"exec", "-i",
"-e", "CLAUDE_BRIDGE_CWD=/home/vscode/claude-bridge",
"-e", "CLAUDE_BRIDGE_DEFAULT_PERMISSION_MODE=bypassPermissions",
"<container>",
"/absolute/path/to/claude-bridge"
]
}
}
}Now every dispatch(...) without an explicit permission_mode runs as
bypassPermissions — no Bash gates, no workspace sandbox. Cowork
doesn't need to know the policy; it's enforced at the bridge. Individual
dispatches can still opt into stricter behavior by passing
permission_mode="acceptEdits" (or "plan") per call.
Only do this when the container is the trust boundary — the container's network and filesystem isolation is what's keeping bypassed permissions safe. Treat the MCP config as privileged: anyone who can edit it can run arbitrary commands in your container as the bridge user. See the official devcontainer guide for the recommended firewall config.
Tools
For agents reading this: call
bridge_help()once at the start of a session. It returns a structured map of every tool, when to use each, the four canonical workflows, and the gotchas that have actually bitten users. Faster than skimming this section.
The tool surface comes in six groups:
Discovery:
bridge_help.Synchronous:
dispatch— short prompts under the MCP ceiling.Asynchronous:
dispatch_async+wait_dispatch/get_dispatch/cancel_dispatch/list_jobs— anything longer. Optional webhook on terminal state.Recurring:
schedule_dispatch+list_schedules/get_schedule/cancel_schedule— fire a prompt every N seconds on a channel until a deadline or the prompt emits the stop sentinel. Supports chaining (after_schedule_id) and webhooks (notify_url).Event log:
list_events— cursor-paged stream of every notable state transition. The "what happened while I was offline?" answer.Completion polling:
list_completions,wait_any_completion— answer "anything new since I last looked?" for finished jobs. Uselist_eventsinstead when you need schedule context.Channel admin:
list_channels,reset_channel.
Synchronous
dispatch(prompt, channel="default", timeout_seconds=300, permission_mode=None, cwd=None)
Run a prompt against claude -p and return the full result. Channels
pin to one Claude Code session each — first call starts fresh,
subsequent calls on the same channel --resume it. Distinct channels
run in parallel; same-channel calls serialize.
permission_mode:default,acceptEdits,plan, orbypassPermissions. Defaults toacceptEdits(override viaCLAUDE_BRIDGE_DEFAULT_PERMISSION_MODE).cwd: per-call override of the working directory. Use it to retarget a single dispatch at a different repo without standing up a second bridge.
Returns {ok, channel, duration_ms, result, session_id, raw} on
success, or {ok: false, channel, duration_ms, error, exit_code} on
failure. Both shapes include stderr when claude wrote to it (e.g.
project-MCP-server warnings). Failures never raise — the MCP layer
always sees a result.
Asynchronous (long-running)
dispatch_async(prompt, channel="default", timeout_seconds=300, permission_mode=None, cwd=None, notify_url=None, notify_on=None, notify_headers=None)
Kick off a dispatch in the background; return a job_id immediately.
Channel locking still applies — concurrent dispatch_async on the same
channel queue up. Empty prompts surface as {"ok": false, "error": ...}
synchronously (no orphan job).
Optional webhook on terminal state: pass notify_url to have the
bridge POST a JSON payload when the job ends. notify_on selects
which terminal states notify (values: done, error, cancelled,
abandoned; default ["done"]). notify_headers adds auth.
Delivery is fire-and-log; failures are recorded as webhook_failed
events. Payload shape: {event, job_id, channel, status, started_at, finished_at, ok, result_preview, error} (result_preview is
truncated to 4KB).
Returns {ok: true, job_id, channel}.
get_dispatch(job_id)
Non-blocking status read. status is one of:
running— work in flight; also includeselapsed_msfor progress.done— full sync-style result keys (ok,result,session_id,duration_ms,raw,stderr, …) plusjob_idandstarted_at.cancelled—cancel_dispatchwas called by the user; subprocess was SIGTERMed.abandoned— the asyncio task running the dispatch was cancelled by the runtime (transport timeout, FastMCP shutdown, loop teardown). The subprocess kept running; a watcher will finalize it when it exits, transitioning the status todone(orerror). Poll again.error— programmer error in the dispatcher itself, or output that couldn't be parsed; should be rare.orphaned— bridge restarted but the subprocess and output files are gone; result was lost. The channel pinning is auto-reset so the next dispatch starts fresh.
Unknown job_id returns {ok: false, error: ...}. Works for both live
jobs and ones loaded from disk after a restart.
wait_dispatch(job_id, max_wait_seconds=50)
Block up to max_wait_seconds for a job, then return whatever
get_dispatch would return. Default 50s is intentionally below the
typical MCP transport ceiling — Cowork polls this in a loop until
status != "running". The underlying job is shielded from cancellation,
so an aborted poll doesn't kill the work in flight.
cancel_dispatch(job_id)
Request cancellation. The task's CancelledError handler kills the
underlying claude -p subprocess before propagating, so we don't leave
orphan workers. Idempotent: {cancelled: true} if cancellation was
requested, {cancelled: false, reason: "already_finished"} otherwise.
list_jobs()
Diagnostics only — returns one summary dict per tracked job (running
and recently finished). Job retention is bounded by max_completed_jobs
(default 1000), so this is safe to call on a long-lived bridge.
Recurring (long-running watch patterns)
schedule_dispatch(prompt, channel, interval_seconds, until=None, until_seconds=None, after_schedule_id=None, notify_url=None, notify_on=None, notify_headers=None, ...)
Fires prompt on channel every interval_seconds, until a deadline
or until the prompt emits the literal stop sentinel
[BRIDGE_STOP_SCHEDULE] in its result. Each tick is its own
dispatch_async job — short individually, collectively long-running.
The bridge owns the loop, persists schedules to disk, and resumes them
after a restart without burst-firing missed ticks (only one tick
fires on the first iteration after a long gap).
interval_secondsminimum is 10s.untilis ISO 8601 ("2026-04-27T20:00:00Z");until_secondsis relative seconds-from-now. Mutually exclusive.If a tick is still running when the next interval fires, the bridge skips that tick (no stacking). Schedules use the same channel for every tick, so ticks share session continuity.
Self-cancellation: have the prompt end with
[BRIDGE_STOP_SCHEDULE]when the watched condition resolves. Example:prompt = "gh pr list --state open. If all merged, end with [BRIDGE_STOP_SCHEDULE]. Otherwise summarize." schedule_dispatch(prompt, channel="pr-watcher", interval_seconds=300, until_seconds=14400)Pipelines — pass
after_schedule_idto chain schedules. The new schedule starts inwaitingand transitions toactivewhen the predecessor reaches a terminal state (completed, cancelled, or error). Cycles are detected and rejected at creation:a = schedule_dispatch(prompt="wave A merge…", channel="wave-a", interval_seconds=300, until_seconds=14400) b = schedule_dispatch(prompt="post-merge hygiene", channel="hygiene", interval_seconds=600, until_seconds=3600, after_schedule_id=a["schedule_id"])Webhooks — pass
notify_urlto get a JSON POST when the schedule reaches notable transitions.notify_onselects events:tick,tick_with_sentinel,tick_error,schedule_end(default["schedule_end"]).notify_headersadds auth. Delivery is fire-and-log; failures are logged aswebhook_failedevents. Payload includesevent,schedule_id,channel,tick_count,status,last_tick_result(truncated to 4KB),last_job_id.
list_schedules() / get_schedule(id) / cancel_schedule(id)
Inspect or stop schedules. cancel_schedule fires the schedule_end
webhook (if configured) and does not cancel the in-flight tick — use
cancel_dispatch(last_job_id) for that.
Event log (turn-level "what happened while I was offline?")
list_events(since=0, limit=100, types=None, notable_only=False)
Bridge-wide structured event stream. Records every state transition:
dispatch lifecycle, schedule lifecycle, webhook outcomes, recovery
actions. The buffer is bounded (default 1000) and persisted to
events.json so it survives restarts.
Two read modes:
Debug mode (
notable_only=False, default): every event, including chatter likedispatch_startandschedule_tick. Right for forensic post-mortem.Surfacing mode (
notable_only=True): only state transitions worth surfacing to a human. Dropsdispatch_start,schedule_tick,schedule_created,webhook_sent,bridge_init_subprocess_alive. Keeps every terminal transition and every failure. This is what an orchestrator wants for "what should I tell the user about?"
Cursor pattern: pass since=0 for everything, then track the largest
ts you've seen. types is an explicit allow-list and composes with
notable_only (intersection). Returns oldest-first.
events = list_events(since=last_seen_ts, notable_only=True)
for e in events: surface(e)
last_seen_ts = max(e["ts"] for e in events) if events else last_seen_tsCall bridge_help() and read notable_event_types to see the curated
set programmatically.
Heads-up: events are only recorded from the moment the event-log feature is running. Anything that fired on a prior bridge process (or before this feature shipped) is gone. Going forward, every state transition is captured.
Completion polling (turn-level "anything new?")
list_completions(since=0, limit=50)
Jobs whose finished_at > since, oldest first. Use since=0 for
"everything that ever finished". For ongoing polling, track the
largest finished_at you've seen and pass it as the next since.
Cheap, non-blocking — safe at the top of every turn.
wait_any_completion(since=0, max_wait_seconds=50)
Long-poll up to max_wait_seconds for any new completion since the
cursor. Same MCP-ceiling-aware default as wait_dispatch. Returns
immediately if any are already available.
Channel admin
list_channels()
{"channels": {channel: session_id, ...}}. Doesn't invoke claude.
Always cheap — Cowork can call this safely while a long dispatch is
running.
reset_channel(channel)
Drops a channel's pinned session so the next dispatch starts fresh.
{"reset": true|false, "channel": ...}. Useful when a project MCP
server (playwright, neon, etc.) has wedged inside the channel's claude
session and you want a clean reconnect.
Polling pattern (the canonical long-running flow)
job = dispatch_async(prompt="...", channel="...", cwd="/workspace",
permission_mode="bypassPermissions",
timeout_seconds=900)
while True:
res = wait_dispatch(job["job_id"], max_wait_seconds=50)
if res["status"] != "running":
break
# optional: surface elapsed_ms, log, or just keep pollingwait_dispatch returning at the 50s mark with status="running" is the
common case for real work; the loop just goes around again. When the
job finishes, wait_dispatch returns immediately with the full result.
Durability guarantees (long workflows)
The bridge is designed so Cowork can trust it across hours-long workflows that span Claude Desktop restarts, container/bridge process restarts, MCP transport hiccups, and Cowork's own per-call timeouts. The contract:
What persists
Channel→session pinning in
sessions.json, atomic temp+rename writes.Every job's state in
jobs.json(same directory) on every transition: spawn (with PID and output-dir), completion, cancellation, abandonment. Concurrent writes are serialized through anasyncio.Lockso parallel dispatches can't lose each other's updates.Subprocess output in
<state>.parent/job-output/<job_id>/{stdout,stderr}, written byclaude -pdirectly (not via pipes the bridge has to drain). Output survives bridge crashes and asyncio task cancellations.
Scheduler + watcher liveness is decoupled from FastMCP
The scheduler task and any orphan-reaping watchers are bootstrapped
by ensure_watchers_running(), which runs at every async tool entry
and on a wall-clock cadence from a daemon supervisor thread. The
thread lives outside FastMCP's asyncio task lifecycle, so even if
FastMCP cancels every in-flight task on a transport disconnect, the
supervisor revives the scheduler within one poll interval (~5s by
default).
Without this, an abandoned tick during a transport blip could leave a schedule "active but not firing" until the next external MCP call woke things up — see issue notes in CLAUDE.md, invariant 25.
Subprocess lifetime is decoupled from the bridge
For dispatch_async, the subprocess is spawned with
start_new_session=True (its own session, immune to SIGHUP) and
stdin redirected to /dev/null. Three follow-on guarantees:
MCP transport timeouts can't kill the work. If FastMCP cancels the asyncio task running a dispatch (because Cowork's MCP request hit a transport timeout, or the connection blipped), the subprocess keeps running and writing output. The job is marked
abandonedrather thancancelled, and a watcher coroutine reaps it when it finishes — finalizing the result from the on-disk output files.Bridge crashes don't kill the work. If the bridge process dies (OOM, supervisor restart, manual kill), the subprocess is parented to PID 1 and keeps running. On bridge restart:
If the PID is still alive, the job stays
runningand a watcher is re-spawned.If the PID has exited and the output files are intact, the job is finalized —
get_dispatch(job_id)returns the recovered result.If neither, the job becomes
orphanedand its channel is unpinned so a new dispatch can't race a stray subprocess on the same session.
cancel_dispatchis the only way to actually stop the work. It sets acancel_requestedflag, SIGTERMs the subprocess by PID (then SIGKILLs if it's still alive on the watcher's next tick), and cancels the asyncio task. Status becomescancelled. Anything else that ends a task (transport timeout, runtime cancellation, asyncio loop teardown) becomesabandoned, notcancelled.
wait_dispatch is shielded
If Cowork's MCP call to wait_dispatch is cancelled by the transport
(e.g. exceeded the per-call ceiling), the underlying job survives —
the inner task is wrapped in asyncio.shield. Cowork retries with the
same job_id and gets the next slice of state.
Failures never raise out of the MCP layer
Subprocess errors, timeouts, missing binary, malformed JSON, runtime
cancellation — all become structured ok: false results. The MCP
transport always sees a clean tool response.
What this does not cover
Container death. If the container itself dies, all subprocesses die with it. Nothing for the bridge to recover.
Output-file corruption (e.g. disk full). If
claude -p's stdout is truncated, the recovery path treats it as a parse failure and marks the job aserror.Subprocess exit code on recovery. When the bridge wasn't the parent at exit, we can't read the return code. Recovery treats well-formed JSON as success and unparseable output as failure — the exit code is informative but not load-bearing.
Configuration (env vars in the container)
Variable | Default | Purpose |
|
| Channel→session map (atomic writes). The same directory holds |
| bridge process cwd | Default working dir for |
|
| Override |
|
| Default if caller omits |
| unset | If set to a path, writes one JSONL line per state transition ( |
| unset | Set to |
| unset | Set to |
Set these via your MCP config (docker exec -e KEY=value ...) or your
devcontainer's containerEnv. They are not negotiated over MCP.
Authentication
The bridge has no auth opinion — it just shells to claude -p and
inherits the container's environment. Anything that makes
claude -p "hi" --output-format json succeed in your container shell
will work here, including:
ANTHROPIC_API_KEYCLAUDE_CODE_OAUTH_TOKEN(fromclaude setup-token)On-disk
~/.claude/.credentials.json(fromclaude /login)
Test (validated by
test_auth_env_passes_through_to_subprocess):
the bridge does not pass env= to the subprocess, so the
container's env reaches claude unchanged.
Troubleshooting
Symptom | Cause | Fix |
| Bridge installed in a venv or | Use the absolute path from |
| The bridge's cwd has a heavy | Anchor the bridge in a clean dir with |
Cowork reports "lost the response handle" or hits a ~60s MCP ceiling on | The MCP transport caps individual tool calls; long claude runs exceed it | Use |
A project MCP server (playwright, neon, cloudflare, …) disconnects mid-session and the channel keeps failing | The wedged MCP server is bound to the pinned |
|
Cowork doesn't see the server after editing the config | Window-close ≠ quit | Fully quit Claude Desktop and relaunch |
Edits to the config produce no servers and no error | JSON syntax error |
|
| That UI is for remote HTTP MCP servers only. Local stdio servers go in the JSON file | (Working as intended — you're not missing anything) |
| MSYS rewrote your |
|
Verifying the bridge directly (optional)
For interactive smoke tests, the only useful CLI check is an MCP
initialize round-trip — there's no --help:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' \
| docker exec -i <container> /absolute/path/to/claude-bridge
# Windows Git Bash: prefix the path with //Expect a one-line JSON response with
"serverInfo":{"name":"claude-bridge",...}. Don't bother running
docker exec ... claude -p "hi" as a sanity check — that's a different
code path than the bridge uses, and it has Windows/Git Bash quirks of
its own.
How it works
┌──────────────────────┐ ┌──────────────────────────────┐
│ HOST │ stdio │ DEVCONTAINER │
│ Claude Cowork ───────┼──────▶│ claude-bridge ─▶ claude -p │
│ (MCP client) │ │ (MCP server) --resume sid │
└──────────────────────┘ └──────────────────────────────┘
docker exec -i …Transport: MCP stdio over
docker exec -i. No network sockets, no port forwarding, no shared bind mounts.Sessions: each channel pins to one Claude Code session id. First call uses
--session-id <new-uuid>; later calls use--resume. We never use--continue— that's the race the original file-queue prototype had to serialize around.Concurrency: distinct channels run in parallel; same-channel calls serialize behind an
asyncio.Lockso message ordering is preserved.State: channel→session map persisted atomically (temp +
os.replace) to~/.claude-bridge/sessions.json.Failure surface: subprocess errors, timeouts, missing binary, bad JSON — all return structured
ok: falseresults, never raise. The MCP layer turns raises into opaqueToolErrors, so this matters.
Security
The container is the trust boundary. Anyone who can
docker execinto it can driveclaudewith whatever auth lives there. Same goes for whoever can editclaude_desktop_config.jsonon the host.acceptEdits(the default) auto-accepts file edits but still prompts forBash. Fine when the prompt doesn't need shell.bypassPermissionsremoves all gates including the workspace sandbox — only safe when the container's network/filesystem isolation is the thing keeping you safe. See Running unattended.
Development
See CONTRIBUTING.md for the full guide. TL;DR:
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest # 83 tests, ~20sTests use a fake claude binary (a Python stub in tests/conftest.py)
so they verify real subprocess behavior — argv formation, JSON parsing,
exit codes, timeouts, env passthrough, per-call cwd, cancellation
handling, schedule firing, and webhook delivery against a loopback
HTTP server — all without a real Anthropic API key.
CI (GitHub Actions) runs the test matrix on Python 3.11 / 3.12 / 3.13 plus a sanity wheel build.
Releases
See CHANGELOG.md. Versioning follows
Semantic Versioning once 0.1.0 is published.
The single source of truth for the version is
src/claude_bridge/__init__.py; pyproject.toml reads it via
hatch's dynamic version mechanism.
Reporting issues / security
General bugs and feature requests: https://github.com/JosiahSiegel/claude-bridge/issues
Security vulnerabilities: see
SECURITY.md.
Available Tools
16 toolsbridge_helpA
Return a structured guide to every tool, recommended workflow, and common gotcha. Call this once when you start using the bridge, or whenever you're unsure which tool fits a situation.
The shape:
tools: name →{group, summary, when_to_use, when_not_to_use}workflows: list of named multi-step patterns with example invocations.concepts: short definitions of channel, session pinning, permission_mode, cwd, the stop sentinel, and the durability model.gotchas: things that have actually bitten users.
Inputs: no named parameters.
cancel_dispatchA
User-cancel a running job. SIGTERMs the subprocess by PID, then
cancels the asyncio task. Status becomes cancelled (vs the
abandoned state for runtime/transport-induced cancellations).
Returns {cancelled: true} if the cancel was actionable, or
{cancelled: false, reason} for jobs that have already finished
or have no live task.
Inputs: job_id (required).
cancel_scheduleA
Stop a schedule from firing further ticks. In-flight ticks are
not cancelled — use cancel_dispatch(last_job_id) for that.
Inputs: schedule_id (required).
dispatchA
Run a prompt against Claude Code in this devcontainer; return the result.
Use for short prompts (under ~60s round trip). For anything longer
use dispatch_async so the MCP transport's per-call ceiling
doesn't abort your tool call before claude finishes.
Args:
prompt: The natural-language task for Claude Code.
channel: Logical conversation thread. Same channel = shared
session (subsequent calls --resume it). Default
"default" — pick a stable channel name per logical
thread (e.g. "feature-auth").
timeout_seconds: Wall-clock seconds before the call is aborted.
Default 300.
permission_mode: default | acceptEdits | plan |
bypassPermissions. Defaults to
CLAUDE_BRIDGE_DEFAULT_PERMISSION_MODE.
cwd: Per-call working directory override. Use it to retarget at
a specific project (e.g. /workspace) while keeping the
bridge anchored in a clean directory for fast cold starts.
Returns:
Success: {ok: true, channel, duration_ms, result, session_id, raw, stderr}.
Failure: {ok: false, channel, duration_ms, error, exit_code?}.
Failures never raise — the MCP layer always sees a result.
Inputs: prompt (required), channel, timeout_seconds, permission_mode, +1 more.
dispatch_asyncA
Spawn a dispatch in the background, return a job_id immediately.
Use this for anything that may exceed the MCP transport's per-call
ceiling (~60s in Cowork) — persona runs, refactors, anything in a
busy project. The subprocess is spawned with its own session
(start_new_session=True) and its stdout/stderr go to files on
disk, so the work survives transport timeouts and bridge restarts.
Then poll with wait_dispatch(job_id) in a loop until
status != 'running'.
Args mirror dispatch. Empty prompts return a structured error
synchronously without creating a job.
Optional webhook notification (notify_url):
The bridge POSTs a JSON payload to notify_url when the job
reaches a terminal state matching notify_on. notify_on
values: done, error, cancelled, abandoned.
Default ["done"]. Delivery is fire-and-log — failures are
recorded in the event log as webhook_failed but don't affect
the job. Add auth via notify_headers.
Payload shape (truncated to 4KB on result_preview)::
{"event": "done", "job_id": "...", "channel": "...",
"status": "done", "ok": true, "started_at": ...,
"finished_at": ..., "result_preview": "...", "error": null}Returns:
Success: {ok: true, job_id, channel}.
Validation failure: {ok: false, error}.
Inputs: prompt (required), channel, timeout_seconds, permission_mode, +4 more.
get_dispatchA
Non-blocking read of a job's current state.
status values:
running— work in flight;elapsed_msincluded for progress.done— full sync-style result keys.cancelled— user calledcancel_dispatch.abandoned— runtime cancel (transport timeout, FastMCP shutdown). Subprocess kept running; watcher will transition this todoneorerrorshortly. Poll again.error— dispatcher-internal error or unparseable output.orphaned— subprocess and output both lost on a restart.
Unknown job_id returns {ok: false, error: ...}. Works for
both live jobs and ones loaded from disk after a bridge restart.
Inputs: job_id (required).
get_scheduleC
Detail view of one schedule. Includes last_job_id so you can
follow up with get_dispatch or list_completions.
Inputs: schedule_id (required).
list_channelsA
channel → pinned session_id. Doesn't tell you whether work is
in flight on a channel — use list_jobs for that.
Inputs: no named parameters.
list_completionsA
Jobs whose finished_at > since, oldest first.
Use since=0 for "everything that ever finished". For ongoing
polling, track the largest finished_at you've seen and pass it
as the next since. Cheap and non-blocking — safe to call at the
start of every turn / iteration to surface "anything new?".
Returns {completions: [<get_dispatch shape>, ...]} with
finished_at present on each entry. raw is omitted; fetch
full payloads via get_dispatch(job_id).
Inputs: since, limit.
list_eventsA
Return bridge events whose ts > since, oldest first.
The event log records dispatch starts/ends, schedule ticks, sentinel hits, schedule completions, webhook outcomes, and recovery actions. The buffer is bounded (default 1000 events) and persisted across bridge restarts.
Two read modes:
Debug mode (
notable_only=False, the default): every event. Good for forensic post-mortem of a workflow.Surfacing mode (
notable_only=True): only state transitions worth reporting to a human — terminal dispatch states, schedule completions / cancellations / errors, webhook failures, recovery actions. Skipsdispatch_start,schedule_tick,schedule_created,webhook_sent, andbridge_init_subprocess_alive. This is what an orchestrator wants for "what should I tell the user about?".
Cursor pattern: pass since=0 for everything, then on each
subsequent call pass the largest ts you saw.
types is an explicit allow-list — composes with
notable_only (intersection).
Common types:
Dispatch lifecycle:
dispatch_start,dispatch_end,dispatch_cancelled,dispatch_abandoned,dispatch_error,dispatch_orphan_finalized.Schedule lifecycle:
schedule_created,schedule_tick,schedule_activated,schedule_self_cancelled,schedule_cancelled,schedule_completed,schedule_tick_error.Webhook outcomes:
webhook_sent,webhook_failed.Recovery:
bridge_init_recovery,bridge_init_subprocess_alive.
Each event has ts (epoch seconds), event (type), plus
type-specific fields (job_id, schedule_id, channel,
ok, duration_ms, etc.).
Note: events generated before this feature shipped are gone — the
in-memory buffer only captures from the current bridge process
onward. Anything you persisted before then lives in the optional
CLAUDE_BRIDGE_LOG JSONL file (if you enabled it).
Inputs: since, limit, types, notable_only.
list_jobsA
All tracked jobs (running and finished). raw is stripped from
done-state summaries to keep the response cheap; use
get_dispatch(job_id) for the full payload.
Inputs: no named parameters.
list_schedulesB
Every schedule the bridge knows about — active, completed, cancelled, error.
Inputs: no named parameters.
reset_channelA
Drop a channel's pinned session so the next dispatch starts a fresh Claude Code session. Useful when a project MCP server has wedged inside the channel's session and you want a clean reconnect.
Does not cancel running work — use cancel_dispatch for that.
Inputs: channel (required).
schedule_dispatchA
Recurring dispatch: fire prompt on channel every
interval_seconds, until a deadline or until the prompt emits
the literal stop sentinel [BRIDGE_STOP_SCHEDULE] in its result.
Each tick is an independent dispatch_async job — ticks are
short individually, but the schedule can run for hours. The bridge
owns the loop, persists schedules to disk, and resumes them after
a restart (without burst-firing missed ticks).
Args:
prompt: The text fired at each tick.
channel: Channel for every tick. Same-session continuity
across ticks (each tick --resumes the prior).
interval_seconds: Seconds between ticks. Minimum 10.
until: Absolute end time, ISO 8601. Prefer including a timezone
(e.g. "2026-04-27T20:00:00Z"); naive datetimes without
a timezone are interpreted as UTC, not local time, so
the same string yields the same instant regardless of which
container the bridge runs in. Mutually exclusive with
until_seconds.
until_seconds: Relative end time in seconds from now (e.g.
14400 = 4 hours).
timeout_seconds: Per-tick timeout. Default 300.
permission_mode: Same as dispatch.
cwd: Same as dispatch.
after_schedule_id: Chain this schedule to start only after the
named predecessor terminates (completed, cancelled, or
error). Useful for pipelines: "after wave A merges, run
hygiene check wave B." This schedule starts in waiting
status and transitions to active automatically. Cycles
are detected and rejected.
notify_url: HTTPS endpoint to POST event payloads to. Optional.
Bridge fires fire-and-log POSTs; the destination is the
user's relay (Slack/Pushover/email/etc.) — bridge does not
retry.
notify_on: List of event names to push. Values:
tick (every tick fired — chatty),
tick_with_sentinel (the tick that triggered self-cancel),
tick_error (a tick failed to spawn),
schedule_end (any terminal transition: completed,
cancelled, error). Default ["schedule_end"].
notify_headers: Extra request headers (e.g. auth). Sent on
every webhook POST.
Returns:
{ok: true, schedule_id, schedule} or {ok: false, error}.
Self-cancellation: if any tick's result text contains
[BRIDGE_STOP_SCHEDULE], the schedule cancels after that tick.
Use this in your prompt for "watch X until Y" patterns:
"Check open PRs. If all merged, end your reply with
[BRIDGE_STOP_SCHEDULE]. Otherwise summarize."Webhook payload shape (result_preview truncated to 4KB)::
{"event": "schedule_end", "schedule_id": "...",
"channel": "...", "tick_count": 17, "status": "cancelled",
"last_tick_at": ..., "last_tick_result": "...",
"last_job_id": "...", "error": null}
Inputs: prompt (required), channel (required), interval_seconds (required), until, +8 more.
wait_any_completionA
Long-poll up to max_wait_seconds for any new completion since
the cursor. Returns immediately if any are already available;
otherwise waits.
Default 50s is below the MCP transport ceiling so you can re-enter
in a loop. Useful when watching schedule ticks land without
polling each tick's job_id separately.
Inputs: since, max_wait_seconds.
wait_dispatchA
Long-poll a job for up to max_wait_seconds.
Default 50s is intentionally below the typical MCP transport
ceiling (~60s) — call this in a loop and break when status != 'running'. The underlying job is shielded from cancellation, so
if your MCP call is aborted at the ceiling the work keeps running
and you can re-enter with the same job_id.
Inputs: job_id (required), max_wait_seconds.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 16 tools
Each tool has a clearly distinct purpose, from dispatching and monitoring jobs to managing schedules and channels. Detailed descriptions further clarify any potential overlap.
All tool names follow a consistent verb_noun pattern in snake_case (e.g., cancel_dispatch, list_schedules, wait_dispatch). The main dispatch tools are named appropriately as core actions.
16 tools is well-scoped for a bridge to Claude Code, covering help, dispatch, scheduling, monitoring, and channel management without being excessive or insufficient.
The tool surface provides comprehensive coverage for the bridge's purpose: running prompts, handling async jobs, scheduling, monitoring, and cancellation. No obvious gaps are present.
Related MCP Connectors
Shared memory MCP server for Claude Code, Codex, Gemini CLI and Cursor. Cited, editable.
Remote MCP server for supportsheep: run AI interviews and manage support content for your blog.
Remote MCP server for supportsheep: run AI interviews and manage support content for your blog.
Related MCP Servers
- AlicenseNot gradedqualityFmaintenanceThe self-hosted MCP bridge between Claude Chat and Claude Code.46AGPL 3.0
- AlicenseAqualityFmaintenanceMCP server for running external coding agents as background tasks inside Claude Code. Supports multiple backends including Codex, Grok, GLM, DeepSeek, and more.7MIT
- AlicenseAqualityCmaintenanceA stdio MCP server that lets Codex Desktop/CLI delegate implementation tasks to Claude Code CLI with workspace validation, Git status checks, and session resume capabilities.24MIT
- AlicenseAqualityDmaintenanceAn MCP server that lets Claude Code delegate durable background tasks, reasoning profiles, thread resumption, and native image generation to your local Codex CLI.101MIT