Agent Bridge
Use Agent Bridge as a local MCP/SQLite message bridge between Claude Code and Kimi CLI for task exchange, results, and bounded autonomous dialogues.
Send messages, replies, and task/result/notice conversation messages with deduplication and advisory response deadlines.
Receive unread messages, optionally long-poll, and acknowledge them with
mark_read.Atomically claim/renew/release message leases so concurrent workers do not execute the same task.
Start and manage bounded conversations: set goal, turn limits, and expiry; inspect status; complete, cancel, or escalate to
needs_human.Manage the current agent’s background listener: enable, disable, inspect status, or get payload-safe progress.
Operate entirely locally over MCP stdio and SQLite, with sender identity fixed by
AGENT_ID.
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., "@Agent Bridge send Kimi a task to review the auth module and reply with findings"
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.
Agent Bridge
Local MCP message bridge for Claude Code and Kimi CLI.
Agent Bridge lets two coding agents exchange tasks, results and bounded autonomous conversations through one shared SQLite database. It uses MCP stdio transport and local files only — no TCP/UDP server, no external service and no network protocol of its own.
The reference Claude Code setup runs the Claude-side worker against Strata, model qwen3.8-flash-next-unsloth-ud-q4_k_xl, through the local launcher. The bridge core itself is model-agnostic and can dispatch work to any compatible agent client.
Status: experimental. The main exchange flow is implemented and covered by tests, but the project is still evolving. See TODO.md for known gaps.
Stack
Claude Code worker: local headless Claude Code launcher, configured for Strata, model
qwen3.8-flash-next-unsloth-ud-q4_k_xl.Kimi CLI worker: official Kimi CLI in prompt mode.
Agent Bridge core: Python 3.11+, MCP stdio transport and SQLite.
Related MCP server: Project Relay
Highlights
Two-way agent inbox: send, receive, acknowledge and reply to messages.
Safe concurrent workers: atomic claims with expiring leases prevent two workers from executing the same task.
Bounded conversations: goal-driven
task → resultdialogues with a turn limit, expiry, server-side ordering and reviewer decisions.Automatic headless delivery: per-agent listeners can dispatch root tasks and conversation turns to one-shot worker processes.
Progress indication: a compact listener status shows the current lifecycle stage and the last dispatch result without exposing task payload.
Anti-spam primitives: response deadlines, idempotency keys and no automatic retries.
Payload-safe diagnostics: logs and status files contain ids, stages and exit codes — not prompts, message text, tokens or worker output.
Single-file state: the shared message database is plain SQLite with WAL mode and busy timeouts.
How it works
Claude Code session ── MCP stdio ──► server.py (AGENT_ID=claude) ─┐
├──► bridge.db
Kimi CLI session ───── MCP stdio ──► server.py (AGENT_ID=kimi) ───┘
▲
│ scan / dispatch
systemd/listener.py
│
systemd/on_message.py
│
one-shot Claude or Kimi workerEach MCP server process derives its sender identity from AGENT_ID; tool arguments cannot impersonate another sender. Both sides point at the same bridge.db, but use opposite values of AGENT_ID. Agent ids are fixed (claude and kimi, AGENTS in server.py); the name cclocal elsewhere in this README is only the local launcher/integration name and the report prefix for the claude side — AGENT_ID=cclocal is rejected.
Interactive MCP sessions do not receive push notifications: an active agent checks receive_messages when it is already running. The optional listener adds the headless path: it scans the database, runs on_message.py for one dispatchable message and waits until that supervisor exits before scanning again.
Requirements
Linux or another POSIX environment
Python 3.11+
Claude Code and/or Kimi CLI, depending on the side being automated
Quick start
git clone <your-repository-url>
cd agent-bridge
uv syncRun the safe dispatcher test suite:
uv run python systemd/test_dispatcher.pyStart a bridge server manually for development:
AGENT_ID=claude uv run python server.py
# in another terminal:
AGENT_ID=kimi uv run python server.pyserver.py is an MCP stdio process, so normally it is launched by the agent client rather than used directly as an interactive command.
MCP registration
Use an absolute repository path. Registration is project-scoped; restart the agent session after adding or changing the MCP server.
Claude Code
claude mcp add-json agent_bridge -s project '{
"command": "uv",
"args": [
"run",
"--directory",
"/absolute/path/to/agent-bridge",
"python",
"server.py"
],
"env": {
"AGENT_ID": "claude"
}
}'Start a new Claude Code session, approve the project-scoped server and check that eight agent_bridge tools are available.
Kimi CLI
Add the following to the project-scoped Kimi MCP configuration, changing the absolute path as needed:
{
"mcpServers": {
"agent_bridge": {
"command": "uv",
"args": [
"run",
"--directory",
"/absolute/path/to/agent-bridge",
"python",
"server.py"
],
"env": {
"AGENT_ID": "kimi"
},
"enabled": true,
"startupTimeoutMs": 30000,
"toolTimeoutMs": 40000,
"enabledTools": [
"send_message",
"receive_messages",
"claim_message",
"release_message",
"mark_read",
"start_conversation",
"conversation",
"listener"
]
}
}
}Start a new Kimi session from the project and trust the workspace if the MCP command is expected.
Tools
Tool | Purpose |
| Store a message, optionally linked with |
| Return unread messages for the current |
| Atomically claim or renew a message lease before headless execution. |
| Return an unfinished claim to the shared inbox without acknowledging it. |
| Acknowledge completed messages; only the recipient can acknowledge. |
| Create a bounded dialogue and send its root |
| Inspect, complete, cancel or escalate a dialogue. |
| Enable, disable or inspect the current agent's background listener; |
Basic exchange
1. sender: send_message(recipient, text, dedup_key="task-1")
2. recipient: receive_messages()
3. recipient: claim_message(message_id)
4. recipient: send_message(sender, result, reply_to=message_id,
dedup_key="task-1:result")
5. recipient: mark_read([message_id])
6. sender: receive_messages()
7. sender: mark_read([result_id])If the recipient cannot finish, it sends a blocked: ... result and releases the original message instead of acknowledging it. A response deadline is advisory metadata: Agent Bridge never auto-resends and never retries an attempted dispatch, and a passed deadline does not stop a never-attempted message from being delivered.
Bounded conversations
start_conversation(recipient, goal, max_turns=6, expires_in_seconds=14400) creates a dialogue with a fixed goal, a turn budget and an expiry time. Messages inside the conversation use kind:
taskasks the other side to perform the next step;resultreports completion, blockage or a request for clarification;noticecarries operational information without waking a worker or consuming a turn.
The server enforces pairing and direction: a result answers a task, a follow-up task answers a result, and only one message may be in flight. The reviewer side must choose exactly one terminal action after a result: complete the conversation, send one concrete follow-up task or escalate to needs_human.
Terminal conversations accept no new tasks or results. Hard dispatch failures freeze the dialogue so a broken chain cannot continue automatically.
Automatic delivery and progress
The default automation gate is:
{
"auto": {
"claude": true,
"kimi": false
},
"dispatch_max_age_seconds": 86400
}Claude-side headless execution is enabled by default in this configuration. Kimi-side headless execution remains disabled because Kimi CLI prompt mode applies its own automatic permission policy without interactive approvals; enable auto.kimi only as a deliberate local decision.
dispatch_max_age_seconds (default 24 h) bounds automatic delivery by message age. A message that arrived while the listener was stopped, restarting or busy with another worker is still dispatched when it comes back, even if its response_deadline has passed — the deadline is metadata for the worker and the human. Only a backlog older than the bound is left alone: listener.py pending lists it and a manual on_message.py <id> runs one message on purpose.
A malformed config.json automates nothing (fail closed).
Useful commands:
uv run python systemd/listener.py status --agent claude
uv run python systemd/listener.py progress --agent claude --once
uv run python systemd/listener.py watch --agent claude
uv run python systemd/listener.py pending --agent claude
uv run python systemd/listener.py disable --agent claude
uv run python systemd/listener.py enable --agent claudelistener status keeps the original machine-readable fields and additionally reports:
stage:stopped,starting,idle,received,supervising,legacy,unknownorerror;progress_state: why a stage is not the ordinary one —unreadable(the progress file exists but cannot be parsed),missing_file(a live listener past the startup grace never wrote one: old code),no_format_version(its document predates the version marker),stale_pid(the document was written by a different process); null on the normal stages;progress: the currently trusted lifecycle record;last: the previous dispatch outcome, including duration and exit code where available.
listener pending is a read-only backlog report: never-attempted messages that could wake a worker, split into dispatchable (the listener will take them) and stale (older than dispatch_max_age_seconds). It prints ids and ages only and never writes to the database.
From an MCP session the same human-readable snapshot is one call: listener(action="progress") returns {"line": ...} — the exact string the cclocal status line renders, e.g. [claude] idle | last msg#43 sender=kimi result=succeeded in 21.5s rc=0. It is ids, enums and numbers only, never task payload, and the call never raises.
Progress files live in systemd/runtime/<agent>.progress.json and contain ids, enums and numbers only. Every document opens with "format_version": 2; a document without that marker comes from a listener written before the marker existed, and status() reports it as legacy instead of guessing its stage. The runtime directory is private and gitignored.
Claude Code status line
There is no supported API for injecting an arbitrary external event into a live Claude Code session. A supported read-side option is Claude Code's custom status line: the command runs on a timer and its stdout becomes the status line.
Create the script in the config directory your interactive sessions actually use — for this machine's cclocal setup that is ~/.claude-local (CLAUDE_CONFIG_DIR=$HOME/.claude-local); on a default Claude Code install it is ~/.claude. Use absolute paths inside, make it executable, and keep it exit 0 unconditionally — a status line command that fails or hangs degrades the whole session UI:
#!/usr/bin/env bash
# ~/.claude-local/statusline-agent-bridge.sh — one line about the claude listener.
BRIDGE=/absolute/path/to/agent-bridge
PY="$BRIDGE/.venv/bin/python"
[ -x "$PY" ] || PY=python3
"$PY" "$BRIDGE/systemd/listener.py" progress --agent claude --once 2>/dev/null
exit 0chmod +x ~/.claude-local/statusline-agent-bridge.shThen add this key to the settings.json in that same config directory, leaving every other key untouched:
{
"statusLine": {
"type": "command",
"command": "/home/<user>/.claude-local/statusline-agent-bridge.sh",
"refreshInterval": 2
}
}The line carries only ids, stage words, sender names, durations and exit codes. Right after a listener restart, before the first dispatch, it is just:
[claude] idleOnce work flows, the same command grows the current-dispatch and history segments:
[claude] supervising msg#33 sender=kimi supervisor=564040 for 12s
[claude] idle | last msg#33 sender=kimi result=blocked in 938.7s rc=0Limitation: the status line is visible only inside interactive Claude Code sessions. Outside a session there is no indication — no desktop notification, sound or dbus integration is provided, by design. A running session may not pick up settings.json changes mid-flight; open a new session to see the line.
This is optional local configuration; Agent Bridge does not modify global Claude settings by itself — enabling it is a deliberate per-machine decision.
Worker launchers
The supervisor chooses a launcher by recipient:
systemd/cclocal_headless.shstarts a non-interactive Claude Code worker. It is an example integration for a local Claude-compatible wrapper and expects the local environment variables used by that wrapper.systemd/kimi_headless.shstarts the official Kimi CLI in prompt mode and passes the task through a private request file so task text does not appear in argv or the process list.
For public or team use, review these launchers and adapt them to your own client wrappers and permission model. Do not weaken static deny rules or hide permission requests.
Workers run in the workspace directory — the project both CLIs open, where their project MCP configs live (.mcp.json for the Claude worker's --strict-mcp-config, .kimi-code/mcp.json for Kimi). By default it is the directory that contains this repository, which fits a layout like ~/project/agent-bridge. If the clone lives elsewhere, set AGENT_BRIDGE_WORKSPACE in the env of both MCP registrations; the listener and its supervisors inherit it.
If a launcher cannot be started at all (missing file, wrong permissions, bad workspace), the attempt is recorded as failed with error_kind=spawn:<Error>, the sender gets the usual payload-free notice and a conversation is frozen — the message is not left silently in spawned.
Headless worker permissions
Claude-side workers start with --permission-prompts none: there is no human to approve anything, so every action that is not already allowed is denied automatically. A worker's effective permission set is rebuilt at process start from three sources:
User settings from
CLAUDE_CONFIG_DIR(cclocal_headless.shsets it from the local wrapper env). In this deployment that file carries nopermissionsblock at all.Project settings for the worker's cwd —
<workspace>/.claude/settings.local.json(--workspace/AGENT_BRIDGE_WORKSPACE, see above). This file is the entire allowlist picture: bothpermissions.allowandadditionalDirectories. MCP configuration is separate (--mcp-config+--strict-mcp-config) and grants nothing.Sandbox auto-approval for simple read-only commands (
ls,cat,grep, …) whose paths stay inside the workspace andadditionalDirectories. Everything else — arbitrary interpreters (python3 -c), script execution, writes, or any path outside those boundaries (evenls -lof a symlink that resolves into~/.local/share/uv/…) — requires approval and is therefore denied headless.
Minimal sets per task type:
Ordinary two-way task (read code, answer over the bridge): the
mcp__agent_bridge__*tools plus sandboxed read-only commands are enough. The worker must be able to finish either way —resultorblocked— without writing anything.Engineering task (edits in the repo): add file rules matching the target paths, plus explicit
Bash(...)entries for every interpreter invocation the task uses (uv run …,.venv/bin/python …,bash script.sh) — the sandbox never auto-approves them. If those rules are absent, the dispatch endsblockedafter burning the whole worker run; mark such tasks "interactive session" instead of dispatching them.
Denial behavior:
Compound commands die whole.
allowed && denied,allowed; deniedorallowed | deniedare denied as a unit — the allowed parts produce nothing. Probe suspect and safe commands in separate calls or you lose the data they could have given.A denial is not fatal. After the first write denial an honest worker stops retrying, files a
blocked:report and releases the claim (required by the dispatcher prompt); the supervisor classifies a self-filed blocker as an honest stop, not a dispatch error.
Why an extended allowlist can appear to "not reach" the worker (TODO P1-11): the worker does read <workspace>/.claude/settings.local.json — proven by additionalDirectories taking effect in headless sessions and by Bash allow rules matching (uv run python systemd/listener.py … runs). What did not match were file rules written with single-slash absolute paths: Edit(/home/<user>/project/**). Edit/Read rules use gitignore-style paths anchored to the settings file's directory, so a leading / anchors the pattern inside that directory and the rule matches nothing; absolute paths in these rules require the // prefix (//home/<user>/project/**), or write them relative to the settings directory (Edit(agent_bridge/**)). Workers 35 and 49, both started after the file was extended, were still denied Edit/Write under agent_bridge/** because of this, while additionalDirectories (which does accept real absolute paths) worked — the mixed effect is what looked like "the worker saw the old set". Confirmed live on 2026-11-03: a headless Edit of agent_bridge/README.md was denied with this rule in place; a positive test of the fixed rule form is still pending (one dispatch after the rule is rewritten).
Reliability model
One attempt barrier:
dispatch_attempts.message_idis the primary key. A rescan, restart or duplicate listener cannot launch the same message twice.No automatic retry: crash, timeout or uncertainty remains visible for a human decision; a short operational notice is sent without task payload.
Evidence-based success: exit code 0 alone is not success. The protocol requires an acknowledged original plus the appropriate result, blocked report, follow-up or terminal conversation state.
Lease and timeout: worker claims expire; a worker timeout kills only that worker process group.
Delivery window: never-attempted messages are dispatched regardless of
response_deadline, withindispatch_max_age_seconds; older ones wait for a human.Crash recovery: stale attempts are reconciled by process evidence and report evidence. A recorded worker pid only counts as alive when
/procshows that process started together with its attempt, so a recycled pid cannot pin an attempt inspawned.--recoverexamines only messages that have never been attempted and uses the same dispatch set as the listener.Concurrent start: every process switches the database to WAL through the same retried recipe (
systemd/schema.py), because on a not-yet-WAL file SQLite rejects the switch immediately while another writer holds a lock.Fresh database bootstrap: the listener creates the dispatcher schema additively before scanning, so a new bridge database does not require a manual supervisor run.
Security notes
The bridge is local: MCP stdio, SQLite and local subprocesses.
Do not commit
bridge.db, runtime files, outbox reports or agent MCP configuration.Secrets are passed through environment variables or private files and are never written to the database, prompts, logs or progress files.
Logs are operational metadata only and are created
0600(dispatcher.logby both the listener and the supervisor,errors.logby the server). Do not redirect MCP stderr into a file that also stores secret-bearing diagnostics.Claude-side workers may run with permission prompts disabled. Keep their allowlist minimal and prefer an explicit
blockedresponse over any uncertain action.Kimi-side automatic dispatch is deliberately gated off in the default configuration.
Conversations are not authorization to perform external or irreversible actions. Deployments, pushes, purchases and messages to third parties require a human decision.
Development
Safe tests use temporary databases and fake launchers/listeners/workers:
uv run python systemd/test_dispatcher.pytest_bridge.py is a destructive end-to-end test that deletes and recreates bridge.db. Run it only in a disposable copy of the project before live traffic, never against a database containing real messages.
Current safe suite: 57 dispatcher tests, including a deterministic stress test for concurrent supervisors against a non-WAL database held by a writer.
systemd/schema.py is the shared core of the dispatcher: DDL, the connection recipe, the config.json reader and the single definition of the dispatch set used by both the listener and --recover. Change dispatch rules there, not in the callers.
Known limitations
See TODO.md. The main open items are operational: minimal permissions for headless workers and visibility of blocked outcomes.
License
Available Tools
8 toolsclaim_messageA
Atomically claim an unread message for this MCP server process.
An active claim by another process cannot be taken over, acknowledged or released. Repeating the call as the current owner renews the lease. An expired claim can be taken by any process belonging to the recipient. lease_seconds is clamped to 30..3600; expiry is checked during tool calls, without background timers.
Inputs: message_id (required), lease_seconds. Returns: object
conversationA
Inspect or steer a conversation only its two agents belong to.
status — state, turn_count/max_turns, expiry, recent transitions and the in-flight message; never the goal text, never message text (no payload, no secrets). cancel — stop all further automatic dispatch for it (side effects already produced are NOT rolled back); allowed from active/blocked/needs_human. complete — the initial goal is verifiably achieved (the review worker's outcome a; only from active). needs_human — a human decision is required (outcome c; only from active). Repeating the call for the state the conversation is already in is an idempotent no-op; a different terminal state is rejected, so the two agents can never fight over the outcome.
Inputs: action (required), conversation_id (required). Returns: object
listenerA
Manage this agent's own background listener (AGENT_ID only).
status: enabled/running/pid and the id of the message the listener is currently waiting on — never task payload. enable: removes the disabled marker and idempotently starts the listener (duplicate sessions create no duplicate — the listener holds an exclusive flock). disable: writes the marker; the listener then takes no new task, lets the current worker finish and exits. No PID is ever killed from here. Dispatch itself stays governed by systemd/config.json auto..
Inputs: action (required). Returns: object
mark_readA
Acknowledge messages by id; only their recipient (AGENT_ID) can do so.
Already-acknowledged, foreign and actively claimed-by-other-worker ids are skipped silently. Free messages and expired claims remain compatible with the original recipient-only acknowledgement behavior.
Inputs: message_ids (required). Returns: object
receive_messagesB
Return available unread messages addressed to AGENT_ID, oldest first.
Messages actively claimed by another server process are hidden. Waits up to wait_seconds, capped at MAX_WAIT_SECONDS (30). Messages stay unread until mark_read. Each message carries response_deadline, claim_expires_at and owned_by_current_worker; cursor is the last returned id, or None.
Inputs: limit, wait_seconds. Returns: object
release_messageA
Release this process's active claim without acknowledging the message.
Inputs: message_id (required). Returns: object
send_messageA
Send a message to another agent; sender is this process's AGENT_ID.
reply_to optionally links this message to the id of a previous message (use it to answer requests). response_timeout_seconds (clamped to 1..3600) is persisted as response_deadline = now + timeout: pure metadata for the receiver — nothing is timed, deleted or resent automatically; after a deadline report it and wait for human direction, late replies stay valid. dedup_key (e.g. a task id): repeating the same (sender, dedup_key) returns the existing message with duplicate=true and inserts nothing. Returns the message id either way.
Conversation messages belong to a bounded autonomous dialogue (see start_conversation): pass conversation_id and kind — 'task' (substantive work that wakes the recipient's listener), 'result' (report on a task; wakes the initiator's review worker only while the conversation is active) or 'notice' (informational, never wakes a worker). Omit conversation_id to inherit it from the conversation of reply_to. Conversation sends are validated atomically: the dialogue must be active and unexpired, the message must answer its task/result and go back to that message's sender, exactly one message may be in flight, and each task/result takes one turn (a send past the turn limit moves the conversation to needs_human and is rejected). A send_message without kind is a plain message and never becomes autonomous.
Inputs: text (required), recipient (required), kind, reply_to, +3 more. Returns: object
start_conversationA
Start a bounded autonomous conversation with another agent.
Creates a conversation (goal, initiator = AGENT_ID, recipient, state active) and posts its root task as turn 1, so the recipient's listener picks it up without any manual phrase. The two agents then exchange task/result messages inside this conversation until one marks it completed or needs_human, the turn limit or expiry is reached, or it is cancelled. The limit counts every automatic worker turn (result reviews included); max_turns is clamped to 2..20 (hard cap 20) and the expiry window to 60..86400 s. A worker reaching the limit or a stopped state must not spawn new work: the dialogue goes to needs_human for a human. dedup_key makes repeated starts idempotent (existing root task, duplicate=true). An ordinary send_message never creates a conversation.
Inputs: goal (required), recipient (required), dedup_key, max_turns, +2 more. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 8 tools
Most tools target clearly distinct operations: listener controls the local daemon, send_message emits, receive_messages reads, and claim/release/mark_read handle message ownership and acknowledgement. The main soft spots are send_message versus start_conversation (start implicitly posts a root task) and receive_messages versus claim_message, though the descriptions spell out the difference reasonably well.
Six of eight tools follow a clear verb_noun pattern (send_message, start_conversation, claim_message, release_message, receive_messages, mark_read), which is highly readable. The two outliers, 'listener' and 'conversation', are bare nouns, a minor deviation from the otherwise predictable convention.
Eight tools is a well-scoped surface for an inter-agent messaging bridge, with each tool covering a distinct lifecycle step (dispatch, inbox, claim lease, ack, conversation control). No tool looks redundant or gratuitous.
The message lifecycle is well covered end-to-end: send with dedup/reply linking, receive, atomic claim with lease, release, acknowledge, plus bounded conversation start/inspect/steer and listener enable/disable. Minor gaps remain — there is no direct 'get message by id' or 'list conversations' operation, so agents must go through receive_messages or track ids themselves.
Maintenance
Related MCP Connectors
Coordinate multiple AI agents over MCP: atomic claims, leases, shared ledger, handoffs, tasks.
Shared memory and mail for your AI agents. Verified with Claude Code; other MCP clients in testing.
Persistent memory, hybrid search and a goal graph for AI agents, over stdio or remote HTTP.
Real-time chat for AI agents. Claude Code, Cursor, Cline and Codex join channels over MCP.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables Claude Code, Codex and opencode sessions on the same machine to message each other live over a local pipe, with threaded replies and offline queuing, and to delegate or spawn one another as background subagents. Messages persist in SQLite and any running server can take over as broker, so peers stay reachable without a daemon.80 npm3MIT
- AlicenseNot gradedqualityBmaintenanceEnables coding agents in different editors and MCP clients to share messages, project context, notes, task ownership, handoffs, and advisory file claims through a local SQLite-backed coordination server. Supports MCP stdio, MCP Streamable HTTP, a JSON HTTP API, and a local CLI for multi-project agent collaboration.MIT
- AlicenseNot gradedqualityBmaintenanceEnables coding-agent sessions on the same machine to collaborate locally and durably by exchanging requests, replies, and task results through an authenticated loopback server with SQLite-backed storage, joined via CLI or an MCP stdio bridge. It also handles message delivery by priority and wait time, deduplicates idempotent sends, enforces task versioning, and preserves artifacts by content hash.13 npmApache 2.0
- AlicenseNot gradedqualityCmaintenanceEnables two local coding-agent sessions to exchange tasks, reports, and blockers over named channels with enforced turn ownership, session recovery, and SQLite-backed state.MIT