Skip to main content
Glama

aiAgentBridge

Let Claude Code, Codex and Opencode sessions message each other in real time.

Python 3.13+ License: MIT MCP

aiAgentBridge is a small Python hub that every AI coding session connects to as an MCP server. Each session registers under a name, then sends messages to another session by name or broadcasts them to all sessions with *. Sessions can be grouped into rooms, so several independent groups can share one hub without seeing each other. Messages arrive as soon as they're sent, with no polling:

  • Claude Code gets messages pushed into the conversation as <channel> blocks. It uses the same mechanism as the official Telegram plugin.

  • Codex and Opencode call wait_for_message, a long-poll that returns the moment a message arrives.

  • Scripts, CI jobs and bots can post to an HTTP webhook, or join as full agents over a WebSocket.

You can have Claude review what Codex just wrote, have Opencode tell Claude when tests pass, or let a CI job broadcast "build is green" to every open session.


Contents


Related MCP server: Agent Communication MCP Server

How it works

flowchart LR
    subgraph Sessions
        CC[Claude Code]
        CX[Codex]
        OC[Opencode]
    end
    AD[agent-bridge-stdio<br/>adapter]
    HUB((agent-bridge hub<br/>127.0.0.1:8765))
    EXT[Scripts / CI / bots]

    CC -- stdio MCP --> AD
    AD -- WebSocket /ws --> HUB
    CX -- HTTP MCP /mcp --> HUB
    OC -- HTTP MCP /mcp --> HUB
    EXT -- POST /webhook or /ws --> HUB

The hub is one long-running process. It keeps the registry of named agents, routes messages, and queues direct messages for agents that are offline.

Codex and Opencode connect straight to the hub over MCP Streamable HTTP.

Claude Code connects through a small stdio adapter, agent-bridge-stdio. Claude Code talks to HTTP MCP servers with the stateless 2026-07-28 protocol, which has no channel for the server to push. Claude Code does accept pushes from stdio servers, though, so it launches the adapter locally. The adapter holds a WebSocket to the hub and forwards each incoming message to Claude as a channel notification.

One message, start to finish:

sequenceDiagram
    participant X as Codex (codex-1)
    participant H as Hub
    participant A as stdio adapter
    participant C as Claude Code (claude-main)

    X->>H: wait_for_message (blocks)
    C->>A: send_message to codex-1: please review foo.py
    A->>H: send frame over WebSocket
    H-->>X: wait_for_message returns the message
    X->>H: send_message to claude-main: looks good
    H->>A: message frame over WebSocket
    A->>C: notifications/claude/channel
    Note over C: shows up in the conversation as a<br/>channel block from codex-1

Requirements

It works on Windows, macOS and Linux.


Install

git clone https://github.com/umairulh2001/aiAgentBridge.git
cd aiAgentBridge
uv sync

uv sync creates .venv/ and installs two commands into it:

Command

What it is

Windows path

macOS / Linux path

agent-bridge

The hub server

.venv\Scripts\agent-bridge.exe

.venv/bin/agent-bridge

agent-bridge-stdio

The stdio adapter for Claude Code

.venv\Scripts\agent-bridge-stdio.exe

.venv/bin/agent-bridge-stdio

In the rest of this README, <path-to-repo> means the absolute path where you cloned the repository, e.g. C:\Users\you\aiAgentBridge or /home/you/aiAgentBridge.


Start the hub

Run it in its own terminal and leave it running:

uv run agent-bridge
# or call the executable directly:
#   Windows:      .venv\Scripts\agent-bridge.exe
#   macOS/Linux:  .venv/bin/agent-bridge

It listens on http://127.0.0.1:8765. Check it with curl http://127.0.0.1:8765/health (on Windows PowerShell use curl.exe; see Troubleshooting).

Option

Env var

Default

Notes

--host

BRIDGE_HOST

127.0.0.1

Keep it on localhost unless you also set a token

--port

BRIDGE_PORT

8765

--token

BRIDGE_TOKEN

none

Requires Authorization: Bearer <token> on every route

--wait-default

50

Default wait_for_message timeout in seconds

--allowed-host

Extra Host header to accept (repeatable), e.g. mybox:8765

--log-level

info

All state is held in memory, so restarting the hub clears the queues. Clients reconnect on their own. Running it as a background service or at login is up to you; any process supervisor works.


Connect your assistants

Every session needs a unique name. That's the name other agents use to reach it. If two sessions ask for the same name, the second one gets <name>-2 and is told so.

Claude Code

1. Register the adapter. This makes it available in every project:

# Windows
claude mcp add --scope user agent-bridge -- "<path-to-repo>\.venv\Scripts\agent-bridge-stdio.exe" --name claude-main

# macOS / Linux
claude mcp add --scope user agent-bridge -- "<path-to-repo>/.venv/bin/agent-bridge-stdio" --name claude-main

Or check it into one project as .mcp.json. Claude Code expands ${VAR}, so each window can pick its own name:

{
  "mcpServers": {
    "agent-bridge": {
      "command": "<path-to-repo>/.venv/bin/agent-bridge-stdio",
      "args": ["--name", "${AGENT_NAME:-claude}", "--room", "${AGENT_ROOM:-default}"]
    }
  }
}
AGENT_NAME=claude-frontend AGENT_ROOM=shop-app claude --dangerously-load-development-channels server:agent-bridge

2. Start Claude Code with channels enabled:

claude --dangerously-load-development-channels server:agent-bridge
  • The text after server: must match the name you registered the server under (agent-bridge).

  • Channels are a research-preview feature of Claude Code, so the flag may change between versions. Check claude --help and the Claude Code docs if it's rejected.

  • Messages appear as <channel> blocks only in interactive sessions. In claude -p, or without the flag, Claude can still receive everything by calling wait_for_message. The adapter keeps every message, so nothing is lost.

Adapter options:

Option

Env var

Default

--name

AGENT_NAME

claude

Name to register under

--room

AGENT_ROOM

default

Room to join (see Rooms)

--hub

BRIDGE_HUB

ws://127.0.0.1:8765

Hub URL

--token

BRIDGE_TOKEN

none

Prefer the env var: args are stored in plain text in configs

--push

BRIDGE_PUSH

channel

wait turns push off; messages are then only returned by wait_for_message

--log-file

BRIDGE_LOG_FILE

none

Claude Code hides a stdio server's stderr, so use this to debug

  • Start order doesn't matter. If the hub isn't up yet, the adapter keeps retrying (0.5s, backing off to 10s). Until it connects, tool calls return "cannot reach the agent-bridge hub".

  • Why the full executable path instead of uv run? uv run re-syncs the environment before every launch. On Windows that clashes with the running hub's locked files (see Troubleshooting). If you prefer uv, use uv run --no-sync --directory <path-to-repo> agent-bridge-stdio --name claude-main.

  • No push needed? Claude can also connect directly over HTTP in wait mode: claude mcp add --scope user --transport http agent-bridge "http://127.0.0.1:8765/mcp?name=claude-main".

Codex

Add this to ~/.codex/config.toml (Windows: %USERPROFILE%\.codex\config.toml):

[mcp_servers.agent-bridge]
url = "http://127.0.0.1:8765/mcp?name=codex-1"
tool_timeout_sec = 600                     # wait_for_message blocks; the default (~60s) is too short
default_tools_approval_mode = "approve"    # needed for `codex exec`, which can't prompt for approval
# bearer_token_env_var = "BRIDGE_TOKEN"    # if the hub runs with --token

Opencode

Add this to opencode.json (in the project or in your global Opencode config):

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "agent-bridge": {
      "type": "remote",
      "url": "http://127.0.0.1:8765/mcp?name=opencode-1",
      "enabled": true
    }
  }
}

If the hub has a token, add "headers": { "Authorization": "Bearer {env:BRIDGE_TOKEN}" }. Opencode fills in {env:...} from the environment, so the token stays out of the file.

Wake Opencode when a message arrives (optional plugin). Opencode has no push, so a message waits until the model calls wait_for_message. The plugin opencode-plugin/agent-bridge-wake.ts fixes that: it polls the hub's /health for this agent's queued count and, when messages wait and the session is idle, starts a turn telling the model to read and answer them. To install it, copy the file into ~/.config/opencode/plugins/ (Windows: %USERPROFILE%\.config\opencode\plugins\) or a project's .opencode/plugins/, then restart Opencode.

  • It reads the hub URL, name and room from the agent-bridge MCP entry, and the token from BRIDGE_TOKEN (or the entry's Authorization header). Env overrides: AGENT_BRIDGE_WAKE_NAME, AGENT_BRIDGE_WAKE_SERVER, AGENT_BRIDGE_WAKE_INTERVAL (seconds, default 3), AGENT_BRIDGE_WAKE_DISABLE=1.

  • It wakes the most recently active session, so a fresh Opencode needs one prompt first.

  • It follows register and join_room. If the hub suffixed the name (opencode-1-2), the plugin learns it the first time the agent calls whoami or list_agents.

Several windows of the same client: Codex and Opencode configs can't vary the name per window. A second Codex window gets codex-1-2 automatically, or the agent can call register to pick a clearer name.


Rooms

A room is a group id that you choose, such as a project, a feature or a chat id. Sessions in the same room can see and message each other. Sessions in other rooms are invisible to them: list_agents, send_message and the * broadcast never cross rooms. Names are unique per room, so claude-main can exist in two rooms at once. A session is in one room at a time. Without a room, it joins default, so a hub where nobody sets a room behaves as before.

Room ids are 1–64 characters of a-z 0-9 _ . : - and are case-insensitive. For example, shop-app, proj:auth and -100123456 are all valid.

Pick the room when connecting:

Client

How

Claude Code (stdio adapter)

--room shop-app, or AGENT_ROOM=shop-app

Codex / Opencode / HTTP

Add &room=shop-app to the URL, e.g. http://127.0.0.1:8765/mcp?name=codex-1&room=shop-app, or send an X-Agent-Room header

WebSocket

ws://127.0.0.1:8765/ws?name=my-bot&room=shop-app

Or switch rooms at runtime with the join_room(room) tool. Codex and Opencode configs have one fixed URL, so this is how two Codex windows end up in different rooms. Ask the agent, for example, "call join_room with room shop-app".

  • join_room is refused while you have unread messages. Read them with wait_for_message first. This prevents replying to a sender from the old room and reaching a same-named agent in the new room.

  • Your queued messages don't move with you, and your old name stops existing in the old room.

  • If your name is taken in the new room by an active session, you get name-2, as when connecting.

  • A Codex or Opencode session that reconnects starts again in the room from its URL.

Outside senders (webhook, send-only WebSocket) pass "room" in the body or frame; it defaults to default. They alone may use "room": "*" together with "to": "*" to broadcast to every room.

NOTE

A room id is not a password. /health lists every room, and any client that can reach the hub can join any room. Rooms keep groups apart, but they don't keep anyone out. Use --token for access control.


Quick start

  1. Start the hub: uv run agent-bridge.

  2. Open Codex and ask:

    Call whoami, then wait_for_message with timeout_s=300. When a message arrives, answer it with send_message to its sender.

  3. Open Claude Code with channels enabled and ask:

    Use agent-bridge to ask codex-1 what 2+2 is.

  4. Codex's wait returns, Codex replies, and the answer appears in Claude's conversation as a <channel source="agent-bridge" from="codex-1"> block.

To see who's connected at any point: curl http://127.0.0.1:8765/health, or ask any agent to call list_agents.


Tools

Every connected session gets these MCP tools:

Tool

Description

whoami()

Your name, room, push mode, whether push is ready, number of queued messages

register(name)

Rename yourself (1–32 chars: a-z 0-9 _ . -). Queued messages move with you and the old name stops existing

list_agents()

Every known agent in your room: name, room, client, online, push mode, queued count

join_room(room)

Move to another room. Refused while you have unread messages (see Rooms)

send_message(to, content, reply_to?)

to is a name in your room, or "*" for every online agent in your room except you. Returns a delivery report: pushed, queued, dropped_oldest, unknown

wait_for_message(timeout_s?, max?)

Blocks until messages arrive and returns them oldest first. timeout_s=0 only checks the inbox. remaining > 0 means call again

Each message looks like this:

{"id": "75915e47e1d4", "from": "codex-1", "to": "claude-main", "content": "looks good",
 "ts": "2026-10-03T07:59:40Z", "reply_to": "05ca99dd3850", "room": "default"}

pushed in a delivery report means the message was handed to the recipient's connection, not that the model has read it.


Webhook and WebSocket API

POST /webhook

# macOS / Linux, or Windows with curl.exe
curl -X POST http://127.0.0.1:8765/webhook \
     -H "Content-Type: application/json" \
     -d '{"from":"ci","to":"*","content":"build is green"}'
# Windows PowerShell
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8765/webhook -ContentType "application/json" `
  -Body '{"from":"ci","to":"claude-main","content":"build is green"}'
  • The body is from (optional), to (a name or *), content, and optionally reply_to and room (default default; "*" with to: "*" reaches every room).

  • The sender is recorded as ext:<from>, so outside callers can't impersonate a registered agent.

  • The endpoint returns the delivery report. A name that isn't in the room returns 404; if the name exists in other rooms, the hint names them. Bad input returns 400.

GET /health

Returns {"status": "ok", "agents": [...]} for every room, each agent with its room. Add ?room=<id> to list one room.

WebSocket /ws

Connecting to ws://127.0.0.1:8765/ws?name=<name>&room=<room> registers you as an agent (add &token=... if the hub has one; room defaults to default). Leave out name to connect as a send-only producer. A producer sends to the frame's room, falling back to the URL's room and then default.

You send

Hub replies

{"type":"send","to":"...","content":"...","reply_to":"..."}

{"type":"report", ...delivery report}

{"type":"list"}

{"type":"agents","you":"...","room":"...","agents":[...]} (your room only)

{"type":"whoami"}

{"type":"whoami","name":"...","room":"...","client":"..."}

{"type":"register","name":"..."}

{"type":"registered", ...}

{"type":"join","room":"..."}

{"type":"joined","name":"...","room":"...", ...}

  • Add "id": <anything> to a frame, and the reply carries the same id.

  • Incoming messages arrive as {"type":"message", "id": ..., "from": ..., "content": ...}.

  • Errors come back as {"type":"error","error":"..."}.

A minimal agent in Python:

import asyncio, json, websockets

async def main():
    async with websockets.connect("ws://127.0.0.1:8765/ws?name=my-bot") as ws:
        print(json.loads(await ws.recv()))                      # {"type": "registered", ...}
        await ws.send(json.dumps({"type": "send", "to": "claude-main", "content": "hello from my-bot"}))
        async for raw in ws:
            frame = json.loads(raw)
            if frame["type"] == "message":
                print(f"{frame['from']}: {frame['content']}")

asyncio.run(main())

Delivery rules

  • Rooms: every rule below applies within one room. A name in another room counts as unknown.

  • Direct messages to an offline agent (one known from an earlier connection) are queued: up to 500 per agent, after which the oldest is dropped. They're delivered when a session claims that name in the same room again. A session that comes back in a different room doesn't get them. Names that stay offline expire after 24 hours.

  • Messages to an unknown name are rejected (reported as unknown), not queued.

  • Broadcasts (to: "*") reach only agents in the room that are online right now, and never the sender.

  • Order is first-in, first-out for each recipient. If a push connection drops, messages queue up and are flushed in order when it comes back.

  • Name clashes:

    • If the current holder is still active, a newcomer gets name-2. Active means its push connection is open, it's blocked in wait_for_message, or it made a request in the last 2 minutes.

    • If the holder is stale, the newcomer takes over the name and its queued messages.

    • A stale session that comes back is renamed on its next call and told so.

  • Vanished clients: a client that disconnects in the middle of wait_for_message is noticed within about a second. It gives up its name and never swallows a message.

  • Slow clients: a push that takes longer than 2 seconds is abandoned and the message is queued instead. One stuck client never delays the others.

  • Persistence: everything is held in memory.


Security

  • The hub listens only on 127.0.0.1 and has DNS-rebinding protection (it rejects foreign Host headers).

  • Use --token / BRIDGE_TOKEN if other users or programs on the machine shouldn't connect, and always if you bind to anything other than localhost.

  • There's no proof of identity. A client that can reach the hub can claim a stale name and read that name's queue.

  • Rooms aren't access control. Anyone who can reach the hub can join any room; see Rooms.

  • Treat messages from other agents as untrusted input. The server's instructions tell agents not to run destructive commands or reveal secrets just because a peer asked, and to check with their user first.

WARNING

There is no loop guard. Two agents told to "always reply" will keep talking to each other and burning tokens. Tell agents not to answer plain acknowledgements, and watch the first few exchanges.


Troubleshooting

Symptom

Cause / fix

No <channel> blocks appear in Claude Code

Channels show up only in interactive sessions started with --dangerously-load-development-channels server:agent-bridge, not in claude -p. Messages aren't lost: call wait_for_message (pushed ones are marked pushed: true).

Claude's whoami shows push_mode: "wait" and client: "claude-code"

Claude is connected over HTTP, which can't push. Use the stdio adapter.

Codex says "MCP tool call requires approval, but approval policy is never"

Add default_tools_approval_mode = "approve" to the [mcp_servers.agent-bridge] block.

codex exec sits at "Reading additional input from stdin…"

It's waiting for stdin to close. In scripts, run codex exec ... </dev/null.

Codex's wait_for_message fails after ~60s

Set tool_timeout_sec = 600, or pass a shorter timeout_s.

Your session got name-2

Another active session holds that name in the same room. Close it, wait about 2 minutes, or call register.

send_message says unknown, but the agent is connected

It's in another room. Compare whoami on both sides, then join_room.

join_room says you have unread messages

Call wait_for_message (timeout_s=0 is enough) until it returns nothing, then try again.

Windows: uv sync / uv run fails with "being used by another process"

The running hub locks .venv\Scripts\*.exe. Stop the hub and run uv sync again. If a sync failed halfway, agent-bridge-stdio.exe may be missing, and re-running uv sync restores it.

Tool calls return "cannot reach the agent-bridge hub"

The hub isn't running, or --hub points at the wrong host or port.

curl on Windows PowerShell behaves strangely

In Windows PowerShell 5, curl is an alias for Invoke-WebRequest. Use curl.exe or Invoke-RestMethod.

401 unauthorized

The hub has a token. Send Authorization: Bearer <token>, or ?token= on WebSockets.

You need to see what the adapter is doing

Start it with --log-file <path>; its stderr isn't visible inside Claude Code.


Development

uv sync
src/agent_bridge/
  hub.py          # routing core: rooms, named mailboxes, sessions, queues, wait/flush (no MCP imports)
  mcp_server.py   # MCP tools, session binding, channel push over Streamable HTTP
  web.py          # Starlette app: /mcp, /webhook, /ws, /health, token auth
  stdio_shim.py   # agent-bridge-stdio: stdio MCP server that proxies to the hub over WebSocket
  __main__.py     # agent-bridge CLI

mcp is pinned to 2.3.x because the code relies on a few private parts of the SDK:

  • Connection.notify, to send custom notifications

  • ServerRequestContext.session._connection

  • the transport's _request_streams, to detect open push streams and disconnected callers


License

MIT © 2026 Umair

Related MCP Connectors

Related MCP Servers