Let Claude Code, OpenCode, Codex, and Antigravity (agy) talk to each other — over MCP, locally, bidirectionally.
open-agent-bridge is a local communication hub for AI agents. It provides service discovery, bidirectional messaging, and MCP tool exposure so that Claude Code, OpenCode, Codex, and Google Antigravity CLI (agy) running in separate projects can delegate tasks, exchange context, and coordinate work without leaving the local machine.
Sessions can be scoped by --identity: every launcher accepts an identity namespace, and agents only see channel messages from peers sharing the same namespace (default global). This gives each ticket/agent its own private inbox.
Table of Contents
# 1. Install the CLI globally (from a cloned repo) — builds and puts `oab` on your PATH
bash bin/install.sh
# 2. Set up your project — writes .mcp.json and starts the registry in the background
cd /path/to/your/project
oab init # interactive wizard (identity + clients)
# 3. Launch Claude Code wired to the bridge
oab claude --identity devThat's it — no dedicated terminal for the registry, no manual .mcp.json editing. Claude Code now has six MCP tools: agent_bridge_guide, list_agents, channel_inbox, channel_clear, message_client_session, reply.
Prefer explicit commands? The wizard is optional:
oab up # registry in the background (idempotent)
oab mcp config --write --identity dev # write/merge .mcp.json with an identity
oab claude --identity dev # launch Claude with the channel enabled
oab status # registry health + agents + .mcp.json
oab doctor # diagnose your environment
oab down # stop the background registryFor OpenCode, install the push plugin (the wizard does this for you when you select it):
oab opencode install-plugin --project .- Local-first, zero infrastructure. Everything runs on
localhost. No cloud relay, no auth tokens, no subscriptions. The registry binds to:4999; agents bind to:5001+. - Native MCP integration. Exposes the agent network as first-class MCP tools via
stdiotransport, so Claude Code, OpenCode, Codex, Antigravity, and other MCP clients can use the same bridge tools. - Bidirectional channel, not fire-and-forget. Messages carry a
conversationId, delivery ACKs are tracked in SQLite, and thereplytool closes the loop back to the sender. Claude Code can ask Codex a question and receive the answer in the same conversation thread. - Per-identity inboxes. Every launcher accepts
--identity <ns>. Sessions only see channel messages from peers in the same namespace — a hard wall, even for directed messages.list_agentsis filtered to the namespace too. Default namespace isglobal. - Bounded inbox, no saturation.
channel_inboxis capped (limit=25, previews only by default) so a flood of pending messages never buries new ones, andchannel_clearplus an automatic terminal sweep keep each inbox light. - OpenCode plugin bridge. The
opencode install-plugincommand installs a local OpenCode plugin that registers an OpenCode bridge client, opens a registry WebSocket, and injects incoming channel messages into the active OpenCode session withsession.prompt_async. - Codex app-server bridge.
CodexAppServerBridgeinjects incoming channel messages directly into the Codex app-server viaturn/startJSON-RPC, so Codex actually processes requests rather than just receiving raw text. - Antigravity native push.
AntigravityLsBridgeService(antigravity ls-push) delivers channel messages straight into a liveagyTUI through its Cascade Language Server (SendUserCascadeMessage) — native push, no tmux, no manual "check inbox".
Claude Code / OpenCode / Codex / Antigravity (agy) / Dashboard
|
v
┌─────────────────────────────────────┐
│ RegistryServer :4999 │
│ HTTP /agents /health /channel/* │
│ WS /ws (relay + broadcast) │
│ SQLite ~/.open-agent-bridge/registry.sqlite│
└──────┬─────────────┬───────────────┘
│ │
v v
┌────────────────┐ ┌──────────────────────────┐
│ AgentServer │ │ McpAgentBridge (stdio) │
│ :5001+ │ │ list_agents │
│ A2A JSON-RPC │ │ channel_inbox │
│ Skills │ │ channel_clear │
│ AG-UI SSE │ │ message_client_session │
│ │ │ reply │
└────────────────┘ └──────────────────────────┘
┌──────────────────────────────────────────────┐
│ CodexAppServerBridge (separate daemon) │
│ Spawns: codex app-server (:4500) │
│ Connects: WS to registry + WS to app-server │
│ Injects channel messages via turn/start │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ OpenCode plugin bridge │
│ Auto-loads from .opencode/plugins/ │
│ Connects: WS to registry │
│ Injects via session.prompt_async │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ AntigravityLsBridgeService (ls-push daemon) │
│ Discovers agy's Cascade Language Server port │
│ Connects: WS to registry │
│ Injects via SendUserCascadeMessage (live TUI)│
└──────────────────────────────────────────────┘
Runtime components — AgentServer, McpAgentBridge, CodexAppServerBridge, AntigravityLsBridgeService, and the OpenCode plugin bridge — connect independently to the registry. None depends on the others being present.
Message flow — Claude Code delegates a task to Codex:
# sequence
Claude Code
calls message_client_session MCP tool
McpAgentBridge posts ChannelMessage to RegistryServer /channel/messages
RegistryServer broadcasts via WS to all subscribers
CodexAppServerBridge receives channel.message event
CodexAppServerBridge calls turn/start on codex app-server
Codex processes the turn and produces a reply
CodexAppServerBridge sends reply ChannelMessage back to registry
McpAgentBridge receives channel.message with the reply
McpAgentBridge surfaces reply in channel_inbox
Claude Code calls reply tool to close the conversation thread
- Node.js
>=22 - pnpm
>=9andgit(to build and install) - (Optional)
claude,codex,opencode, oragyin$PATHfor the clients you want to bridge
# 1. Prerequisites
node -v # must be >= 22
npm install -g pnpm # if you don't have pnpm
# 2. Clone
git clone <repo-url>
cd open-agent-bridge
# 3. Build + put `oab` / `open-agent-bridge` on your PATH (one step)
bash bin/install.sh
# equivalent to: pnpm install && pnpm run build:all && npm install -g .
# 4. Verify
oab --version
oab doctor
# 5. Use it in any project
cd /path/to/your/project
oab init # wizard: identity + clients → writes .mcp.json + starts the registry
oab claude --identity devcd open-agent-bridge
git pull # optional: get the latest
pnpm install # (re)install dependencies
pnpm run build:all # compile CLI + dashboard
npm install -g . # expose oab / open-agent-bridge on PATH
# (these four steps = bin/install.sh)
oab doctor # validate environment + PATHUpdating later:
cd open-agent-bridge && git pull && pnpm install && pnpm run build:all && npm install -g .
oabnot found after install?bin/install.shandoab doctorprint the exactexport PATH=...line and which rc file to add it to. With npm, the global bin dir ($(npm prefix -g)/bin) is usually already on your PATH.Contributor mode (no global install):
pnpm run dev -- <command>still works from the repo (e.g.pnpm run dev -- doctor).
In one step it: detects your project, asks for a channel identity and which clients to wire up, writes/merges .mcp.json (with AGENT_BRIDGE_IDENTITY baked in), starts the registry as a background daemon (no terminal to keep open), configures plugin-based clients (OpenCode/Antigravity), registers the MCP server with Codex via codex mcp add (Codex ignores .mcp.json — it only loads servers from ~/.codex/config.toml), and prints the exact launch command per client.
The registry exposes:
| Endpoint | Description |
|---|---|
http://localhost:4999 |
HTTP REST (/agents, /health, /channel/*) |
ws://localhost:4999/ws |
WebSocket relay and channel broadcast |
http://localhost:4999/dashboard |
Dashboard SPA (requires pnpm run build:all) |
Manage its lifecycle with oab up / oab down / oab status. To run an agent server for richer project skills, use oab start /path/to/project (add --claude for the Claude Code AI backend).
Once the registry is running and .mcp.json is in place, each AI client connects to open-agent-bridge through its own startup command.
The easy way — ensures the registry is up, writes .mcp.json if missing, sets the identity, and launches Claude:
oab claude --identity devEquivalent manual command:
claude --dangerously-load-development-channels server:open-agent-bridgeThe --dangerously-load-development-channels flag tells the Claude CLI to activate the MCP server named server:open-agent-bridge as a development notification channel. The name open-agent-bridge matches the entry in .mcp.json. This enables:
- The six MCP tools (
agent_bridge_guide,list_agents,channel_inbox,channel_clear,message_client_session,reply). - Push notifications via
notifications/claude/channel— incoming channel messages appear as<channel>blocks inline in the terminal.
The MCP adapter registers the Claude Code session automatically on the first initialize handshake.
OpenCode needs two pieces:
- An MCP server entry so the OpenCode model can call
list_agents,message_client_session,channel_inbox, andreply. - The local OpenCode plugin so incoming channel messages can be pushed into the active OpenCode session automatically.
Install the plugin into a project:
open-agent-bridge opencode install-plugin --project "/absolute/path/to/your/project"Or install it globally:
open-agent-bridge opencode install-plugin --globalThe command copies agent-bridge.ts into .opencode/plugins/ (or ~/.config/opencode/plugins/) and merges the plugin dependencies into the matching .opencode/package.json / global package file. Local plugins are auto-loaded by OpenCode, so no opencode.json plugin entry is required. Restart OpenCode after installing.
The plugin registers as an OpenCode bridge client (clientVersion: "opencode-plugin-bridge"), opens a WebSocket to the registry, sends heartbeats, and injects matching channel.message events into the active session through ctx.client.session.prompt_async. It also re-syncs missed pending messages periodically, deduplicates message IDs, and sends delivered_to_bridge / displayed_to_client ACKs back to the registry.
When OpenCode is connected through MCP, list_agents(includeClients=true) shows rows like [OpenCode] and [OpenCode bridge]. Routing is automatic: sending to the OpenCode MCP client is redirected to the sibling plugin bridge when present, while the MCP client can still use channel_inbox and reply.
open-agent-bridge codex start --project "/absolute/path/to/your/project"This single command:
- Starts the registry (if not already running).
- Launches the
CodexAppServerBridgedaemon, which spawnscodex app-serveron:4500. - Registers the Codex client session in the registry.
- Opens the Codex TUI.
From that point, message_client_session routes to the Codex bridge automatically (it has the highest delivery priority).
If you prefer to start Codex separately:
# Terminal A — bridge only
open-agent-bridge codex app-bridge --project "/absolute/path/to/your/project"
# Terminal B — Codex TUI connecting to the app-server
codex --remote ws://127.0.0.1:4500A plain codex command starts an isolated session. The registry may still see an inner MCP client, but the bridge cannot inject automatic turns into that TUI. If no remote TUI attaches to the bridge app-server, queued messages fail with a detail that tells you to run codex --remote ws://127.0.0.1:<port>.
Google's Antigravity CLI (agy) connects through native MCP plus a Cascade Language Server push daemon. There is no ACP and no tmux scraping — agy loads the bridge as an MCP server, and ls-push injects channel messages straight into the live TUI.
1. Install the MCP config into the workspace:
open-agent-bridge antigravity install-plugin --project "/absolute/path/to/your/project"
# add --identity ticket-123 for a private namespace
# add --global to install into ~/.gemini/antigravity-cli instead of the workspace .agents/This writes .agents/mcp_config.json pointing agy at open-agent-bridge mcp start. Restart agy in the workspace so it loads the server. (No hooks.json is written — agy's hook schema differs and the Stop-hook path is superseded by ls-push.)
2. Start the native push daemon so messages arrive in the live TUI automatically:
open-agent-bridge antigravity ls-push --project "/absolute/path/to/your/project"
# match the identity used at install: --identity ticket-123ls-push discovers agy's Cascade Language Server port (from ~/.gemini/antigravity-cli/log/cli-*.log, with an ss fallback), resolves the active cascadeId, and on each pending channel message calls SendUserCascadeMessage so agy processes it as a real user turn — no manual "check inbox".
Requires an open conversation.
agymust be running with an active trajectory in the workspace for the language server to accept an injected turn. Occasionallyagy's model executor errors mid-turn (anagy-internal limitation) and the user nudges it with a plainok.
There are also antigravity hook-stop / antigravity hook-session-start subcommands available for hook-driven setups, but ls-push is the recommended delivery path.
By default every session lives in the global namespace and sees all global channel traffic. Passing --identity <name> puts a session in a private namespace: it only sees messages from peers that share the exact same identity, and only those peers appear in its list_agents. This is a hard wall — even a message addressed directly to an agentId is dropped if the namespaces differ.
# MCP launcher (Claude Code, generic clients) — also honours AGENT_BRIDGE_IDENTITY env var
open-agent-bridge mcp start --project /path --identity ticket-123
# Antigravity (install + push must share the identity)
open-agent-bridge antigravity install-plugin --project /path --identity ticket-123
open-agent-bridge antigravity ls-push --project /path --identity ticket-123
# Codex
open-agent-bridge codex start --project /path --identity ticket-123How it works:
identityis folded into the stable agentId hash (name + project + identity), so each(client, project, identity)is a distinct registry entry — two sessions of the same client/project no longer collide.- The hard wall is enforced in every client profile's
acceptsChannelMessage; the inbox reads the local store, which is only populated with accepted (same-namespace) messages. - Outbound messages are stamped with the sender's identity, so a
ticket-123reply stays insideticket-123.
Implication: to delegate to an
agyrunning inticket-123, the sender must also run inticket-123. A multi-ticket coordinator would have to send per namespace.
# One-time: build the Vue SPA
pnpm run build:all
# Open in browser
open-agent-bridge dashboard
# → http://localhost:4999/dashboardFor hot-reload development: pnpm run dev:dashboard (Vite on :5173, proxies API calls to :4999).
open-agent-bridge mcp config generates the .mcp.json entry. The AGENT_BRIDGE_PROJECT environment variable tells the MCP adapter which project to associate the Claude Code session with:
{
"mcpServers": {
"open-agent-bridge": {
"command": "node",
"args": ["/absolute/path/to/dist/cli/index.js", "mcp", "start"],
"env": {
"AGENT_BRIDGE_PROJECT": "/absolute/path/to/your/project"
}
}
}
}For a globally installed version, pass --global:
node dist/cli/index.js mcp config --global --writeWhich produces:
{
"mcpServers": {
"open-agent-bridge": {
"command": "pnpm",
"args": ["dlx", "open-agent-bridge", "mcp", "start"],
"env": {
"AGENT_BRIDGE_PROJECT": "/absolute/path/to/your/project"
}
}
}
}The mcp config command writes absolute paths for the current machine. Commit the result to the project or adapt the same open-agent-bridge mcp start command for your MCP client settings.
| Tool | Description |
|---|---|
agent_bridge_guide |
Built-in MCP usage guide. Returns setup, send/reply workflow, ACK semantics, and troubleshooting by topic. |
list_agents |
Discover connected agents and client sessions in the current identity namespace. Client rows include peer labels like [Claude Code], [OpenCode], [OpenCode bridge], [Codex inner], [Codex bridge], and [Antigravity inner], plus the unique 8-char suffix of the agentId (e.g. [3fdb0c6a]) so two same-project peers stay visually distinct. Bridge routing is automatic. |
channel_inbox |
Inspect channel conversations. Bounded by default (limit=25, previews only) so a flood of pending messages never buries new ones; the response carries a summary block and a replyWith hint per pending entry. |
channel_clear |
Clear handled conversations from the inbox so new ones keep surfacing. Scope answered / failed / all (never bulk-clears unanswered work) or a specific conversationId. |
message_client_session |
Send a message to a named client session. Automatically resolves OpenCode/Codex inner clients to their bridge daemon when available and infers expectsResponse when omitted. |
reply |
Respond to an incoming channel message, correlating by conversationId. |
Check live tool and agent status at any time:
pnpm run dev -- mcp statusThe MCP instructions block intentionally stays compact. The deeper guide — peer-type semantics, send/reply patterns, troubleshooting delivered_to_bridge stalls, OpenCode plugin setup, and Codex --remote setup — lives in skill files loaded on demand by each client:
- OpenCode:
.opencode/skills/open-agent-bridge/SKILL.md(project-local, auto-scanned by OpenCode) - Codex:
~/.codex/skills/open-agent-bridge/SKILL.md(global Codex skill)
Each skill file includes a references/ folder with peer-type taxonomy, send/reply patterns, delivery states, routing rules, troubleshooting ladder, and the injection prompt contract.
The same content can also be queried at runtime from any client via the agent_bridge_guide MCP tool — useful for agents that don't load skill files.
Channels are the bidirectional messaging layer of open-agent-bridge. They let any agent or client session — Claude Code, OpenCode, Codex, Antigravity (agy), the dashboard — send and receive structured conversational messages through the registry, with full delivery tracking and SQLite persistence.
This is the core feature of the project. Everything else (MCP tools, bridge daemons, dashboard chat) is built on top of it.
Every message belongs to a conversation identified by a conversationId. A conversation is a thread of related turns between two parties — like a back-and-forth between Claude Code and Codex.
Conversation abc-123
├── Message m1 from: client-dashboard-ui to: client-codex-bridge-xyz "Review routes.ts"
├── Ack a1 state: delivered_to_bridge
├── Message m2 from: client-codex-bridge-xyz to: client-dashboard-ui "Found 2 issues…"
└── Ack a2 state: answered
Key properties:
conversationIdis stable for the entire thread — use it to continue an existing conversation.replyTolinks a message to the specificmessageIdit responds to.expectsResponse: truemarks a message as pending until a reply arrives. In MCP sends, the adapter defaults totruewhen you omit the field (agent-to-agent messages are a conversation, not a log stream). Only explicit fire-and-forget markers in the text —FYI,no reply,sin respuesta,just letting you know, etc. — downgrade it tofalse. Pass the flag explicitly for deterministic behavior.requiresAck: truerequests an explicit delivery acknowledgement from the recipient.- All messages and ACKs are persisted in SQLite and survive registry restarts.
interface ChannelMessage {
conversationId: string; // Thread identifier — stable per conversation
messageId: string; // Unique per message (UUID)
fromAgentId: string; // Sender's registry ID (required)
fromAgentName?: string; // Human-readable sender name
toAgentId?: string; // Recipient's registry ID (omit for broadcast)
replyTo?: string; // messageId this turn responds to
taskId?: string; // Optional task association
kind: "chat" | "task_request" | "task_result" | "ack" | "error" | "presence";
content: string; // Message body
meta?: Record<string, unknown>; // Arbitrary metadata
createdAt: number; // Unix ms timestamp (set by registry)
expiresAt?: number; // Expiry timestamp in ms
requiresAck?: boolean; // Request delivery acknowledgement
expectsResponse?: boolean; // Sender awaits a reply (default true; FYI markers downgrade to false)
attemptCount?: number; // Delivery attempt counter (for retries)
}Each message transitions through delivery states tracked via ChannelAck records:
queued
└─→ delivered_to_bridge (bridge daemon received it)
└─→ displayed_to_client (client session received it)
├─→ answered (recipient replied)
└─→ failed (delivery or reply failed)
ACK records carry: conversationId, messageId, state, actorId, actorType (registry | bridge | client | agent), and timestamp.
These are the six tools Claude Code gets after configuring open-agent-bridge as an MCP server.
Read the usage guide exposed by the MCP server itself. This is the quickest way for an agent to refresh the protocol contract without relying on external docs.
Parameters:
topic? "overview" | "setup" | "send" | "reply" | "acks" | "troubleshooting" | "all"
Section to return (default: "all")
Discover what agents and client sessions are currently connected.
Parameters:
skill? string — Filter to agents that expose this skill
project? string — Filter by project name or path substring
healthyOnly? boolean — Only show healthy agents (default: true)
includeClients? boolean — Include passive client sessions (default: false)
Send a channel message to a client session. The tool resolves the best target automatically.
Parameters:
message string — Message body (required)
project? string — Project name or path to identify the target session
clientId? string — Exact agentId of the target (skips all resolution)
clientType? string — Disambiguate when a project has multiple sessions
("claude-code" | "opencode" | "codex" | "antigravity")
conversationId? string — Continue an existing conversation thread
replyTo? string — messageId this message responds to
taskId? string — Associate with a task
expectsResponse? boolean — Whether you expect a reply. Default: true. Only explicit
FYI/no-reply markers in the text downgrade it to false.
timeoutMs? number — Ms before the message expires without a reply
expectsResponse controls whether the receiver should answer:
- Default is
true. Agent-to-agent channel messages are a conversation contract — the receiver should reply unless the sender opts out. A bare"hola"or"build done"will wake up OpenCode/Codex's bridge (or push into a liveagyTUI) with a "reply required" prompt, and Claude Code will surface it as a pending conversation. - Pass
false(or include explicit FYI markers —FYI,no reply,sin respuesta,just letting you know,for your information,no need to reply) for fire-and-forget. The bridge daemon will inject the message as informational context and suppress the receiver's outbound reply. - For deterministic workflows pass the flag explicitly — text inference is a convenience for proactive sends, not a contract.
The mapping from message text to the boolean lives in inferExpectsResponse and is unit-tested under src/__tests__/peer-type-label.test.ts.
Target resolution order:
clientIdprovided → direct lookup, no further resolution- No
clientIdand noproject, butconversationIdprovided → resolves from conversation history projectprovided → filter by project path/name match- Multiple matches → sorted by client type priority: bridge daemons first (
app-server-bridge,opencode-plugin-bridge), thenclaude-code>claude>opencode>codex-cli>codex>antigravity
Returns: toAgentId, conversationId, messageId, deliveryState.
Inspect channel conversations. By default it shows only pending (awaiting-reply) conversations and is bounded so a backlog of messages can never saturate the agent's context and bury new arrivals.
Parameters:
pendingOnly? boolean — Only show conversations awaiting reply (default: true).
Set false for the GLOBAL view of all tracked conversations.
expiredOnly? boolean — Only show conversations past their expiry
limit? number — Max conversations returned (default: 25). Applies to ALL views.
includeMessages? boolean — Include each conversation's full message history
(default: false — only previews + replyWith are returned, to keep
the inbox light). Set true, ideally with a narrow view, to read threads.
The response is wrapped with a summary block plus the conversations array. When the result is truncated, the summary hints at channel_clear and the global view:
{
"summary": {
"view": "pending",
"total": 42,
"shown": 25,
"truncated": true,
"hint": "Use channel_clear({scope:'answered'|'failed'|'all'}) to clear handled threads so new ones surface, or raise `limit`. Use channel_inbox(pendingOnly=false) for the global view."
},
"conversations": [
{
"conversationId": "abc-123",
"status": "pending",
"lastMessagePreview": "Review src/api/routes.ts for N+1 queries",
"replyWith": {
"agentId": "client-codex-bridge-xyz",
"conversationId": "abc-123",
"replyTo": "msg-456"
}
}
]
}Clear handled conversations from this agent's inbox so saturation never buries new messages. Suppresses them from channel_inbox (reversible at the registry) and removes them from the local store. Only ever touches the calling agent's own tracked conversations.
Parameters:
scope string — "answered" → clear only answered threads
"failed" → clear failed + locally-expired threads
"all" → clear every NON-pending thread (never bulk-clears unanswered work)
<conversationId> → clear exactly that one, even if pending (explicit intent)
In addition to manual clearing, the registry runs an automatic terminal sweep: answered/failed conversations older than the retention window are pruned periodically so every inbox stays light without intervention.
Respond to a pending channel message. Use the replyWith values from channel_inbox.
Parameters:
agentId string — fromAgentId of the message you're replying to (required)
conversationId string — conversationId from channel_inbox replyWith (required)
replyTo string — messageId from channel_inbox replyWith (required)
message string — Your reply content (required)
taskId? string — Task association (optional)
skillId? string — Fallback skill for task invocation (optional)
The registry exposes a REST API at http://localhost:4999.
Agent management:
| Method | Path | Description |
|---|---|---|
POST |
/agents |
Register a new agent |
DELETE |
/agents/:id |
Deregister an agent |
POST |
/agents/:id/heartbeat |
Update agent heartbeat |
GET |
/agents |
List all registered agents |
GET |
/agents/:id |
Get a single agent by ID |
POST |
/agents/:id/message |
Relay a task message to a specific agent |
POST |
/agents/:id/ag-ui |
Proxy AG-UI SSE stream for a specific agent |
GET |
/health |
Registry liveness check |
Channel operations:
| Method | Path | Description |
|---|---|---|
POST |
/channel/messages |
Create and broadcast a new channel message |
POST |
/channel/acks |
Record a delivery acknowledgement |
GET |
/channel/conversations |
List conversations (?pending=true for pending only) |
GET |
/channel/conversations/:id |
Full conversation snapshot with messages and ACKs |
POST |
/channel/conversations/:id/suppress |
Hide a conversation from inbox listings |
DELETE |
/channel/conversations/:id/suppress |
Restore a suppressed conversation |
POST |
/channel/messages/:convId/:msgId/retry |
Re-deliver a message (increments attemptCount) |
POST |
/notify-claude |
Shorthand: send a channel notification to a Claude Code session |
Create a message (POST /channel/messages):
{
"fromAgentId": "my-agent-id",
"toAgentId": "client-dashboard-ui",
"kind": "chat",
"content": "Task complete. Found 3 endpoints.",
"conversationId": "abc-123",
"replyTo": "msg-456",
"expectsResponse": false,
"requiresAck": true
}Connect to ws://localhost:4999/ws to receive real-time channel events. Send { "type": "identify", "agentId": "<your-id>" } immediately after connecting to enable targeted delivery.
| Event type | Payload | When |
|---|---|---|
channel.message |
ChannelMessage |
A new message was posted to the registry |
channel.ack |
ChannelAck |
A delivery state changed |
channel.conversation.suppressed |
{ conversationId } |
A conversation was hidden |
channel.conversation.revived |
{ conversationId } |
A suppressed conversation was restored |
Targeted delivery: If a message has
toAgentId, the registry delivers it directly to that agent's WS connection before broadcasting to all subscribers.
Claude Code asks Codex to review a file, waits for the answer, and closes the thread:
# Step 1 — discover what's connected
list_agents
→ "codex" session active on project /projects/my-app
# Step 2 — send the request
message_client_session(
project: "my-app",
message: "Review src/api/routes.ts for N+1 queries",
expectsResponse: true,
timeoutMs: 120000
)
→ conversationId: "abc-123", messageId: "msg-001", deliveryState: "queued"
# Step 3 — Codex processes the turn via app-server bridge, sends reply
# (happens automatically via CodexAppServerBridge → turn/start JSON-RPC)
# Step 4 — check the inbox
channel_inbox
→ conversationId: "abc-123", status: "pending"
lastMessagePreview: "Found 2 N+1 issues in getUserPosts() and..."
replyWith: { agentId: "client-codex-bridge-xyz", conversationId: "abc-123", replyTo: "msg-002" }
# Step 5 — acknowledge and close the thread
reply(
agentId: "client-codex-bridge-xyz",
conversationId: "abc-123",
replyTo: "msg-002",
message: "Thanks, applying the fix now."
)
→ Reply sent. Conversation answered.
Each AI client has a different bridge mechanism depending on how it receives channel messages. All clients ultimately use the same channel protocol — what differs is how the message surfaces to the human.
Claude Code's bridge is built into the MCP adapter itself — no separate daemon required.
When Claude Code connects to open-agent-bridge via MCP, the McpAgentBridge intercepts the MCP initialize handshake. It reads the client name (Claude Code, version, workspace roots), builds a stable agentId (client-claude-code-{hash}), and registers it as a client session in the registry automatically.
From that point on, any channel message addressed to that agentId is pushed to the Claude terminal as an MCP notification:
Incoming channel message
→ ChannelClientRuntime receives channel.message event via WS
→ ClaudeClientProfile.mapChannelMessage() formats it
→ server.notification({ method: "notifications/claude/channel", params: { content, meta } })
→ Claude terminal renders a <channel> block inline
What Claude sees in its terminal:
<channel source="open-agent-bridge" from_agent="my-agent-id" conversation_id="abc-123" message_id="msg-001">
Task complete. Found 3 N+1 queries in getUserPosts().
</channel>Claude then uses the reply MCP tool to respond, closing the conversation thread.
Delivery ACKs sent automatically:
delivered_to_bridge— MCP adapter received the messagedisplayed_to_client— notification push to Claude succeeded
If Claude Code is not yet connected when a message arrives, the adapter buffers up to 100 messages and replays them on initialize.
Sending to Claude Code from another agent — use the notify-claude skill or POST /notify-claude:
# From any AgentServer via notify-claude skill
{
"targetProject": "/path/to/my-project",
"content": "Build finished. 3 tests failed in auth module.",
"expectsResponse": true
}
# Or directly via HTTP
POST http://localhost:4999/notify-claude
{
"agentId": "my-agent",
"toAgentId": "client-claude-code-abc123",
"content": "Build finished. 3 tests failed in auth module.",
"conversationId": "conv-xyz",
"expectsResponse": true
}OpenCode's bridge is a local plugin installed by:
pnpm run dev -- opencode install-plugin --project /path/to/opencode-project
# or, after build/install:
open-agent-bridge opencode install-plugin --project /path/to/opencode-projectThe installer copies the bridge plugin into .opencode/plugins/agent-bridge.ts. For global use it writes to ~/.config/opencode/plugins/agent-bridge.ts. OpenCode auto-scans local plugins, so the installer does not need to mutate opencode.json for plugin registration.
At runtime the plugin:
- Registers a client row named like
<project> (opencode bridge)withclientVersion: "opencode-plugin-bridge". - Identifies its WebSocket as
client-opencode-bridge-{hash}so the registry can target it directly. - Tracks the active OpenCode session from
session.created,session.updated, andsession.idleevents. - Injects inbound channel messages with
ctx.client.session.prompt_async. - Sends delivery ACKs and periodically re-syncs missed pending messages from
/channel/conversations.
The injected prompt mirrors the Codex format. If expectsResponse !== false, OpenCode receives a "reply required" turn with the exact agent-bridge.reply fields to copy. Informational messages are marked "no reply" and tell the receiving agent not to call reply.
OpenCode can also connect to the MCP adapter as a normal MCP client. In that case OpenCodeClientProfile accepts direct messages for the OpenCode MCP session and sibling bridge messages for the same project, so channel_inbox(pendingOnly=true) still works even when auto-routing chooses the plugin bridge.
The Codex bridge runs as a separate daemon (CodexAppServerBridge) that connects the channel layer to the Codex app-server JSON-RPC protocol:
- Connects to the Codex app-server WebSocket (
ws://127.0.0.1:4500) - Performs the
initializehandshake - Registers as a client session in the registry (
clientName: "codex",clientVersion: "app-server-bridge") - On incoming
channel.message: callsturn/starton the app-server — Codex processes it as a real prompt turn - Codex's reply is sent back through the channel
This is the preferred path when Codex is active: message_client_session automatically routes to the bridge (priority -1) over the Codex TUI MCP client.
One-command startup (registry + bridge + Codex TUI):
pnpm run dev -- codex start --project /path/to/codex-projectBridge daemon only:
pnpm run dev -- codex app-bridge --project /path/to/codex-project
# Start Codex separately with: codex --remote ws://127.0.0.1:4500tmux fallback — when the app-server is not available, inject follow-up prompts into the active Codex pane:
pnpm run dev -- codex tmux-bind # bind current tmux pane to this session
pnpm run dev -- codex tmux-sidecar # poll and inject pending channel messagesTroubleshooting — delivered_to_bridge stuck without a Codex TUI
If Claude Code (or any sender) sees status delivered_to_bridge for a Codex peer but Codex never picks the turn up, the most common cause is that the Codex TUI is running in isolated mode (plain codex) instead of attached to the bridge's app-server. The bridge daemon's app-server has nothing to inject into. Fix by either:
# Option A — let the bridge launch and attach Codex for you
pnpm run dev -- codex start --project /path/to/codex-project
# Option B — start the bridge separately, then attach Codex manually
pnpm run dev -- codex app-bridge --project /path/to/codex-project
codex --remote ws://127.0.0.1:4500The inner MCP client of an isolated codex session still registers with the registry, so channel_inbox(pendingOnly=true) will surface the message — but the agent has to call it manually instead of receiving an automatic turn injection. The full diagnosis ladder lives in the agent-bridge skill.
Antigravity (agy) has no ACP and no app-server. It does, however, spawn a Cascade Language Server (Codeium/Windsurf lineage) on a random localhost port whenever it runs. AntigravityLsBridgeService uses that to push channel messages into the live TUI:
- Discovers the language server's HTTP port — parses the newest
~/.gemini/antigravity-cli/log/cli-*.log(listening on random port at <N> for HTTP), with anss-based fallback, and verifies it viaGetWorkspaceInfos. - Resolves the active
cascadeIdfor the workspace (viaGetAllCascadeTrajectories). - On each pending channel message, calls
SendUserCascadeMessage(POST …/exa.language_server_pb.LanguageServerService/SendUserCascadeMessage) soagyprocesses it as a real user turn — not a passive notification. - Deduplicates by message ID and applies a per-message re-injection blackout (state file
.open-agent-bridge/antigravity-ls-state.json).
The bridge runs as a poller registered as a client (clientName: "antigravity"). It is identity-aware: it resolves the agy session by project + identity and only injects pending messages for that namespace.
Setup:
# 1. Install the MCP config (and restart agy so it loads the server)
open-agent-bridge antigravity install-plugin --project "/abs/path" [--identity ticket-123]
# 2. Run the push daemon (match the identity)
open-agent-bridge antigravity ls-push --project "/abs/path" [--identity ticket-123]Options for ls-push: --registry-url <url>, --identity <id>, --client-id <id>, --poll-interval-ms <n> (default 2000), --retry-interval-ms <n> (default 30000), --once, --verbose.
Limitations (both agy-internal): the language server only accepts an injected turn when agy has an open/active conversation in the workspace; and occasionally agy's model executor errors mid-turn (neither PlanModel nor RequestedModel specified), which the user clears with a plain ok. SendAgentMessage returns 200 but does not trigger a turn — SendUserCascadeMessage is the one that works. Because this is an undocumented internal RPC, it is fully isolated in antigravity-ls-client.ts to bound the blast radius across agy versions.
The Codex bridge daemon uses a prompt template (src/client/injection-prompt.ts) that wraps every inbound channel message before injecting it as a turn. The OpenCode plugin carries the same contract in its local plugin file. (Antigravity uses the language-server push instead, with its own concise wrapper.) The wrapper exists so the receiving LLM can identify the sender, see the full content delimited from instructions, and copy a pre-filled reply call without having to derive any IDs.
A reply-required injection looks like this:
[open-agent-bridge] Channel message — reply required
===============================================
From: rcm-worker (3fdb0c6a)
Conversation: 7734035a-2c10-4626-5761-13f6d02f6e45
Message ID: 1d147308-a831-4525-aaf6-3c41614d5a20
----- BEGIN MESSAGE -----
<sender's content>
----- END MESSAGE -----
▶ Respond to the sender NOW with the agent-bridge MCP. The sender is
waiting on this turn — silence will block them.
Tool call (copy each field verbatim — the adapter handles routing):
agent-bridge.reply
agentId: "client-claude-code-2d1c3fdb0c6a"
conversationId: "7734035a-2c10-4626-5761-13f6d02f6e45"
replyTo: "1d147308-a831-4525-aaf6-3c41614d5a20"
message: "<your answer here>"
How to compose the reply:
1. Do any local work the sender's message implies …
2. Put your answer or finding in the `message` field.
...
When expectsResponse resolves to false (sender opted into FYI), the header switches to Channel message — informational (no reply) and the prompt instructs the receiver to NOT call reply. Claude Code peers do not receive this wrapper — they get a raw <channel> push event and decide what to do based on conversation context.
Test contract: src/__tests__/injection-prompt.test.ts locks the structure (BEGIN/END markers, literal IDs in the call template, header wording) so future edits don't silently regress it.
| Client | Bridge type | How messages arrive | Requires daemon |
|---|---|---|---|
| Claude Code | Built-in MCP push | <channel> block in terminal via notifications/claude/channel |
No — part of MCP adapter |
| OpenCode | Local plugin bridge | session.prompt_async → OpenCode processes as a prompt turn |
No separate process — plugin runs inside OpenCode |
| Codex | App-server daemon | turn/start JSON-RPC → Codex processes as a prompt turn |
Yes — codex app-bridge |
Antigravity (agy) |
Cascade Language Server push | SendUserCascadeMessage → agy processes as a user turn in the live TUI |
Yes — antigravity ls-push |
| Dashboard | WebSocket | Chat panel updates via channel.message WS event |
No — built into registry WS |
All paths share the same channel protocol (ChannelMessage, ChannelAck, conversationId). The bridge layer is just the last-mile delivery mechanism.
The dashboard is a Vue 3 SPA served by the registry at http://localhost:4999/dashboard. It shows live agent status, channel conversations, and task events. Agents display an identity badge and are grouped by their identity namespace, so multi-ticket setups stay legible at a glance.
Build the dashboard:
pnpm run build:dashboard
# or build everything at once
pnpm run build:allOpen in the browser:
pnpm run dev -- dashboard
# Dashboard: http://localhost:4999/dashboardThe dashboard connects to the registry WebSocket for live updates. Pass --no-open to print the URL without launching a browser. For hot-reload development use pnpm run dev:dashboard (Vite on :5173).
Always available on every AgentServer, regardless of project type.
| Skill | Description |
|---|---|
file-search |
Find files by glob pattern within the project directory. |
endpoint-find |
Detect HTTP endpoints in backends or API calls in frontends. |
code-query |
Search source code by text or regex. |
prompt-execute |
Render a prompt template with variables and return the result. |
notify-claude |
Send a notification to a Claude Code terminal via the channel. |
shell-execute |
Run an arbitrary shell command in the project directory. |
Activated based on files found in the project root at startup.
| Skill | Activation condition | Requires --claude |
|---|---|---|
run-script |
package.json present |
No |
run-tests |
Jest, Vitest, pytest, or pom.xml detected |
No |
docker-build |
Dockerfile present |
No |
code-review |
src/ directory present (uses Claude Code AI backend) |
Yes |
claude-execute |
Always registered when --claude is active |
Yes |
📖 Full command reference:
docs/cli-reference.md— every command, option, default, and recipe. The table below is a summary.
Once installed globally, run oab <command> (or open-agent-bridge <command>). From a source checkout, pnpm run dev -- <command> also works.
| Command | Description |
|---|---|
start [path] |
Start an agent server for the given directory (defaults to .). |
registry start |
Start the registry on :4999. |
registry status |
Query registry health and agent count. |
list |
List all active agents. |
health [agent-id] |
Check reachability of one or all agents. |
ask <agent> <message> |
Send a one-shot task to a specific agent. |
find <query> |
Search agents by skill or project type. |
delegate <skill-id> <message> |
Send a task to the healthiest agent exposing a given skill. |
broadcast <message> |
Send a message to all healthy agents. |
mcp start |
Start the MCP adapter in stdio mode (used by Claude Code, OpenCode, Codex, Antigravity, and other MCP clients). Accepts --identity <ns> (or AGENT_BRIDGE_IDENTITY). |
mcp config [--write] [--global] |
Print or write .mcp.json configuration. |
mcp status |
Show live agents and registered MCP tools. |
mcp server |
Start the MCP adapter in HTTP/SSE mode (port 6000). |
opencode install-plugin |
Install the local OpenCode plugin bridge into a project or globally. |
codex start |
One-command: registry + bridge + Codex TUI. |
codex app-bridge |
Start the Codex app-server bridge daemon only. |
codex tmux-bind |
Bind the current tmux pane to the active Codex session. |
codex tmux-sidecar |
Poll and inject pending channel messages into a tmux pane. |
antigravity install-plugin |
Write the agy MCP config (.agents/mcp_config.json) into a workspace or globally. Accepts --identity. |
antigravity ls-push |
Push pending channel messages into a live agy TUI via its Cascade Language Server. Accepts --identity, --poll-interval-ms, --once. |
antigravity hook-stop |
Stop-hook handler: deliver pending messages (hook-driven setups). |
antigravity hook-session-start |
SessionStart-hook handler: surface pending messages as context. |
dashboard |
Print or open the dashboard URL in the browser. |
| Script | Command | Description |
|---|---|---|
build |
tsc + plugin copy |
Compile TypeScript to dist/ and copy the OpenCode plugin files into dist/client/opencode-plugin/. |
dev |
tsx src/cli/index.ts |
Run CLI from source without building. |
start |
node dist/cli/index.js |
Run the compiled CLI. |
clean |
rm -rf dist |
Delete build output. |
build:dashboard |
cd dashboard && pnpm run build |
Build the Vue dashboard SPA. |
build:all |
build + build:dashboard + copy |
Full production build including dashboard. |
dev:dashboard |
cd dashboard && pnpm run dev |
Vite hot-reload dev server for the dashboard. |
test |
vitest run |
Run all unit tests once. |
lint |
biome check src/ |
Lint and check code style with Biome. |
lint:fix |
biome check src/ --write |
Auto-fix lint issues. |
src/
├── agent/
│ ├── server.ts AgentServer: HTTP + WS + A2A JSON-RPC + AG-UI SSE
│ ├── handlers.ts TaskStore, RequestRouter, skill inference
│ ├── card.ts A2A AgentCard builder
│ ├── project-detector.ts Project type detection from filesystem
│ └── ag-ui-events.ts AG-UI SSE event helpers
├── cli/
│ ├── index.ts CLI entrypoint (Commander)
│ └── commands/ One file per CLI sub-command
├── client/
│ ├── a2a-client.ts A2AClient: HTTP + WS client for agent-to-agent calls
│ ├── registry-client.ts RegistryClient: HTTP client for the registry REST API
│ ├── client-profile-resolver.ts Resolves delivery profile by client type (Claude/OpenCode/Codex/Antigravity)
│ ├── conversation-session-store.ts In-memory conversation state with ACK tracking
│ ├── codex-app-server-bridge.ts CodexAppServerBridge daemon
│ ├── codex-app-server-client.ts WS client for the Codex app-server protocol
│ ├── codex-runtime-discovery.ts Detect running Codex process
│ ├── codex-session-files.ts Read/write active Codex session marker
│ ├── codex-tmux.ts Low-level tmux pane helpers for Codex
│ ├── codex-tmux-bridge-service.ts tmux-based Codex injection sidecar
│ ├── antigravity-ls-client.ts Cascade Language Server client (port discovery + SendUserCascadeMessage)
│ ├── antigravity-ls-bridge-service.ts ls-push poller — inject pending messages into live agy TUI
│ ├── antigravity-pending.ts collectPendingForClient helper (identity-scoped)
│ ├── antigravity-runtime-discovery.ts Detect running agy process
│ ├── antigravity-history.ts Read agy conversation/history storage
│ ├── antigravity-agents-file.ts Append original-request context for agy hooks
│ ├── antigravity-hooks.ts Build Stop / SessionStart hook outputs
│ ├── opencode-plugin/ OpenCode local plugin source copied by `opencode install-plugin`
│ ├── channel-transport.ts WebSocket transport to registry (stamps identity)
│ ├── channel-client-runtime.ts WS runtime with reconnect + event bus
│ ├── conversation-service.ts High-level send/reply/inbox helpers
│ └── profiles/ Client behavior profiles (Claude, OpenCode, Codex, Antigravity) — identity hard wall
├── mcp/
│ ├── adapter.ts McpAgentBridge — 6 MCP tools + session resolution + identity + shutdown
│ └── clear-scope.ts Pure scope selector for channel_clear
├── registry/
│ ├── server.ts RegistryServer: HTTP + WebSocket hub (:4999) + terminal-conversation sweep
│ ├── store.ts AgentStore: in-memory only (dedup keyed by client+project+identity)
│ ├── channel-store.ts SQLite persistence for channel messages and ACKs (+ terminal cleanup)
│ └── events.ts RegistryEventBus
├── skills/
│ ├── framework.ts BaseSkill, SkillRegistry
│ ├── state-graph.ts StateGraph for multi-step skill workflows
│ └── builtins/ Built-in and auto-detected skill implementations
└── types/
├── a2a.ts A2A spec types (AgentCard, Task, etc.)
├── messages.ts Registry wire types (AgentMessage, ChannelMessage, etc.)
└── skills.ts Skill context and I/O types
Persistence: channel messages and ACKs are stored in SQLite at ~/.open-agent-bridge/registry.sqlite (per-user, independent of the registry's cwd) across three tables: channel_messages, channel_acks, and channel_suppressed_conversations. SQLite access uses the node:sqlite built-in module (Node 22+) — there is no external SQLite dependency. The AgentStore (registered agents and heartbeats) is in-memory only and resets on registry restart; agents re-register automatically on reconnect.
AgentServer — A2A JSON-RPC methods:
| Method | Description |
|---|---|
message/send |
Submit a task to the agent (A2A 0.3.0; honors message.taskId/contextId, resumes input-required tasks) |
tasks/send |
Legacy alias of message/send (pre-0.3 draft param shape) |
tasks/get |
Poll the status of an in-flight task |
tasks/cancel |
Cancel a running task |
agent.health |
Liveness check (returns uptime, version, skill count) |
agent.hello |
Handshake — returns agent name, project, and capabilities |
project.info |
Project metadata: type, framework, category |
project.files |
List files matching a glob pattern |
project.search |
Full-text / regex search across project files |
| Feature | Status |
|---|---|
| Registry HTTP + WS + SQLite | stable |
| MCP adapter — 6 tools | stable |
| Claude Code push bridge | stable |
| OpenCode plugin push bridge | stable |
| Codex app-server bridge | stable |
Antigravity native push (Cascade Language Server, ls-push) |
stable — requires an open agy conversation |
Identity scoping (--identity, per-namespace inbox + list_agents) |
stable |
Inbox bounding + channel_clear + terminal sweep |
stable |
| Dashboard Vue SPA (identity badges + grouping) | stable |
| 6 built-in skills | stable |
HTTP SSE MCP mode (mcp server, port 6000) |
stable |
| Dynamic skills (run-script, run-tests, docker-build, code-review) | stable — require --claude |
A2A 0.3.0 conformance layer (message/send, agent-card.json, protocolVersion, dual kind/type Parts, Task.kind/contextId) |
stable — legacy draft methods kept as aliases |
message/stream / tasks/sendSubscribe — A2A SSE streaming |
stub — not implemented (card advertises streaming: false) |
| StateGraph skill composition | implemented, unused in production |
ask --stream CLI flag |
declared, not implemented |
pnpm run test # vitest — runs the full suite (60 test files, 420 tests) once
pnpm run lint # biome — lint and style checkTest coverage includes: injection prompt contract (injection-prompt.test.ts), peer-type label generation (peer-type-label.test.ts), inferExpectsResponse text inference, conversation ID determinism, ACK state transitions, MCP adapter routing, identity scoping (identity-scoping.test.ts, stable-agent-id.test.ts), channel_clear scope selection (channel-clear-select.test.ts), the Antigravity language-server client (antigravity-ls-client.test.ts), and the Antigravity history/hooks/profile/runtime modules.
- Local only. The registry, agents, and bridges all run on
localhost. No remote or cloud deployment is supported in v0.1. - Single registry. All agents must connect to the same registry instance. Multi-registry federation is not implemented.
- No authentication. All local connections are unauthenticated. Do not expose registry or agent ports beyond
localhost. - Volatile agent registry. The
AgentStoreis in-memory only. Restarting the registry clears all registered agents and heartbeats — agents re-register automatically on reconnect, but any in-flight state is lost. Only channel messages and ACKs (inchannel_messages,channel_acks,channel_suppressed_conversations) are persisted to SQLite. - Codex bridge requires app-server remote TUI or tmux fallback. The preferred path is
open-agent-bridge codex startorcodex --remote ws://127.0.0.1:<port>against the bridge app-server. A plaincodexsession is isolated and cannot receive automatic turn injection. The tmux sidecar fallback polls at a fixed interval and injects follow-ups as synthetic keypresses, which is inherently racy under heavy TUI use. - OpenCode push requires the local plugin. OpenCode can call the MCP tools without the plugin, but automatic turn injection depends on
open-agent-bridge opencode install-pluginand an OpenCode restart. Without the plugin bridge, inbound work must be discovered throughchannel_inbox. - Dashboard
handleChannelMessagedepends ontoAgentIdin broadcast. When a channel message is broadcast without atoAgentId, the dashboard may not correctly attribute it to the right conversation in the UI — this is a known issue with the current broadcast routing in the registry WebSocket relay. tasks/sendSubscribenot implemented. End-to-end A2A streaming (Server-Sent Events per task) is not yet supported. The method is a stub — it is not announced in the Agent Card.ask --streamflag is a no-op. The--streamoption is declared in the CLI but the handler never reads it. Streaming task output is not implemented.- Antigravity push needs a live conversation + occasional nudge.
antigravity ls-pushinjects viaagy's Cascade Language Server, which only accepts a turn whenagyhas an open/active conversation in the workspace. The underlying RPC (SendUserCascadeMessage) is undocumented and may change betweenagyversions (isolated inantigravity-ls-client.ts).agy's model executor also errors mid-turn intermittently (neither PlanModel nor RequestedModel specified), which the user clears with a plainok. - Identity is a hard wall. Cross-namespace delegation is intentionally impossible — the sender must run in the same
--identityas the recipient. A multi-ticket coordinator has to send per namespace. The registry storage remains a global pool; isolation is enforced by inbox/list_agentsfiltering, not at the registry API.
- Fork the repository and create a feature branch.
- Run
pnpm installandpnpm run buildto verify the build. - Run
pnpm run test(Vitest) andpnpm run lint(Biome) before committing. - Open a pull request describing the change and its motivation.
MIT